tksbrokerapi.TKSBrokerAPI

TKSBrokerAPI is the trading platform for automation and simplifying the implementation of trading scenarios, as well as working with Tinkoff Invest API server via the REST protocol. The TKSBrokerAPI platform may be used in two ways: from the console, it has a rich keys and commands, or you can use it as Python module with python import.

TKSBrokerAPI allows you to automate routine trading operations and implement your trading scenarios, or just receive the necessary information from the broker. It is easy enough to integrate into various CI/CD automation systems.

   1# -*- coding: utf-8 -*-
   2# Author: Timur Gilmullin
   3
   4"""
   5**TKSBrokerAPI** is the trading platform for automation and simplifying the implementation of trading scenarios,
   6as well as working with Tinkoff Invest API server via the REST protocol. The TKSBrokerAPI platform may be used in two ways:
   7from the console, it has a rich keys and commands, or you can use it as Python module with `python import`.
   8
   9TKSBrokerAPI allows you to automate routine trading operations and implement your trading scenarios, or just receive
  10the necessary information from the broker. It is easy enough to integrate into various CI/CD automation systems.
  11
  12- **Open account for trading:** http://tinkoff.ru/sl/AaX1Et1omnH
  13- **TKSBrokerAPI module documentation:** https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSBrokerAPI.html
  14- **See CLI examples:** https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md#Usage-examples
  15- **Used constants are in the TKSEnums module:** https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSEnums.html
  16- **About Tinkoff Invest API:** https://tinkoff.github.io/investAPI/
  17- **Tinkoff Invest API documentation:** https://tinkoff.github.io/investAPI/swagger-ui/
  18"""
  19
  20# Copyright (c) 2022 Gilmillin Timur Mansurovich
  21#
  22# Licensed under the Apache License, Version 2.0 (the "License");
  23# you may not use this file except in compliance with the License.
  24# You may obtain a copy of the License at
  25#
  26#     http://www.apache.org/licenses/LICENSE-2.0
  27#
  28# Unless required by applicable law or agreed to in writing, software
  29# distributed under the License is distributed on an "AS IS" BASIS,
  30# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  31# See the License for the specific language governing permissions and
  32# limitations under the License.
  33
  34
  35import sys
  36import os
  37from argparse import ArgumentParser
  38from importlib.metadata import version
  39
  40from dateutil.tz import tzlocal
  41from time import sleep
  42
  43import re
  44import json
  45import requests
  46import traceback as tb
  47from typing import Union
  48
  49from multiprocessing import cpu_count, Lock
  50from multiprocessing.pool import ThreadPool
  51import pandas as pd
  52
  53from mako.template import Template  # Mako Templates for Python (https://www.makotemplates.org/). Mako is a template library provides simple syntax and maximum performance.
  54from Templates import *  # Some html-templates used by reporting methods in TKSBrokerAPI module
  55from TKSEnums import *  # A lot of constants from enums sections: https://tinkoff.github.io/investAPI/swagger-ui/
  56from TradeRoutines import *  # This library contains some methods used by trade scenarios implemented with TKSBrokerAPI module
  57
  58from pricegenerator.PriceGenerator import PriceGenerator, uLogger  # This module has a lot of instruments to work with candles data (https://github.com/Tim55667757/PriceGenerator)
  59from pricegenerator.UniLogger import DisableLogger as PGDisLog  # Method for disable log from PriceGenerator
  60
  61import UniLogger as uLog  # Logger for TKSBrokerAPI
  62
  63
  64# --- Common technical parameters:
  65
  66PGDisLog(uLogger.handlers[0])  # Disable 3-rd party logging from PriceGenerator
  67uLogger = uLog.UniLogger  # init logger for TKSBrokerAPI
  68uLogger.level = 10  # debug level by default for TKSBrokerAPI module
  69uLogger.handlers[0].level = 20  # info level by default for STDOUT of TKSBrokerAPI module
  70
  71__version__ = "1.6"  # The "major.minor" version setup here, but build number define at the build-server only
  72
  73CPU_COUNT = cpu_count()  # host's real CPU count
  74CPU_USAGES = CPU_COUNT - 1 if CPU_COUNT > 1 else 1  # how many CPUs will be used for parallel calculations
  75
  76
  77class TinkoffBrokerServer:
  78    """
  79    This class implements methods to work with Tinkoff broker server.
  80
  81    Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/
  82
  83    About `token`: https://tinkoff.github.io/investAPI/token/
  84    """
  85    def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None:
  86        """
  87        Main class init.
  88
  89        :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`.
  90        :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports.
  91                          Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.
  92        :param useCache: use default cache file with raw data to use instead of `iList`.
  93                         True by default. Cache is auto-update if new day has come.
  94                         If you don't want to use cache and always updates raw data then set `useCache=False`.
  95        :param defaultCache: path to default cache file. `dump.json` by default.
  96        """
  97        if token is None or not token:
  98            try:
  99                self.token = r"{}".format(os.environ["TKS_API_TOKEN"])
 100                uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/")
 101
 102            except KeyError:
 103                uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/")
 104                raise Exception("Token required")
 105
 106        else:
 107            self.token = token  # highly priority than environment variable 'TKS_API_TOKEN'
 108            uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`")
 109
 110        if accountId is None or not accountId:
 111            try:
 112                self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"])
 113                uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId))
 114
 115            except KeyError:
 116                uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).")
 117
 118        else:
 119            self.accountId = accountId  # highly priority than environment variable 'TKS_ACCOUNT_ID'
 120            uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId))
 121
 122        self.version = __version__  # duplicate here used TKSBrokerAPI main version
 123        """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only.
 124
 125        Latest version: https://pypi.org/project/tksbrokerapi/
 126        """
 127
 128        self._tag = ""
 129        """Identification TKSBrokerAPI tag in log messages to simplify debugging when platform instances runs in parallel mode. Default: `""` (empty string)."""
 130
 131        self.__lock = Lock()  # initialize multiprocessing mutex lock
 132
 133        self.aliases = TKS_TICKER_ALIASES
 134        """Some aliases instead official tickers.
 135
 136        See also: `TKSEnums.TKS_TICKER_ALIASES`
 137        """
 138
 139        self.aliasesKeys = self.aliases.keys()  # re-calc only first time at class init
 140
 141        self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED  # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there
 142
 143        self._ticker = ""
 144        """String with ticker, e.g. `GOOGL`. Tickers may be upper case only.
 145
 146        Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc.
 147        More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`.
 148
 149        See also: `SearchByTicker()`, `SearchInstruments()`.
 150        """
 151
 152        self._figi = ""
 153        """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only.
 154
 155        See also: `SearchByFIGI()`, `SearchInstruments()`.
 156        """
 157
 158        self.depth = 1
 159        """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI.
 160
 161        See also: `GetCurrentPrices()`.
 162        """
 163
 164        self.server = r"https://invest-public-api.tinkoff.ru/rest"
 165        """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest
 166
 167        See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`.
 168        """
 169
 170        uLogger.debug("Broker API server: {}".format(self.server))
 171
 172        self.timeout = 15
 173        """Server operations timeout in seconds. Default: `15`.
 174
 175        See also: `SendAPIRequest()`.
 176        """
 177
 178        self.headers = {
 179            "Content-Type": "application/json",
 180            "accept": "application/json",
 181            "Authorization": "Bearer {}".format(self.token),
 182            "x-app-name": "Tim55667757.TKSBrokerAPI",
 183        }
 184        """
 185        Headers which send in every request to broker server. Please, do not change it!
 186        Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}", "x-app-name": "Tim55667757.TKSBrokerAPI"}`.
 187
 188        See also: `SendAPIRequest()`.
 189        """
 190
 191        self.body = None
 192        """Request body which send to broker server. Default: `None`.
 193
 194        See also: `SendAPIRequest()`.
 195        """
 196
 197        self.moreDebug = False
 198        """Enables more debug information in this class, such as net request and response headers in all methods. `False` by default."""
 199
 200        self.useHTMLReports = False
 201        """
 202        If `True` then TKSBrokerAPI generate also HTML reports from Markdown. `False` by default.
 203        
 204        See also: Mako Templates for Python (https://www.makotemplates.org/). Mako is a template library provides simple syntax and maximum performance.
 205        """
 206
 207        self.historyFile = None
 208        """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame.
 209
 210        See also: `History()`.
 211        """
 212
 213        self.htmlHistoryFile = "index.html"
 214        """Full path to the html file where rendered candles chart stored. Default: `index.html`.
 215
 216        See also: `ShowHistoryChart()`.
 217        """
 218
 219        self.instrumentsFile = "instruments.md"
 220        """Filename where full available to user instruments list will be saved. Default: `instruments.md`.
 221
 222        See also: `ShowInstrumentsInfo()`.
 223        """
 224
 225        self.searchResultsFile = "search-results.md"
 226        """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`.
 227
 228        See also: `SearchInstruments()`.
 229        """
 230
 231        self.pricesFile = "prices.md"
 232        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
 233
 234        See also: `GetListOfPrices()`.
 235        """
 236
 237        self.infoFile = "info.md"
 238        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
 239
 240        See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`.
 241        """
 242
 243        self.bondsXLSXFile = "ext-bonds.xlsx"
 244        """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 
 245        bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`.
 246
 247        See also: `ExtendBondsData()`.
 248        """
 249
 250        self.calendarFile = "calendar.md"
 251        """Filename where bonds payment calendar will be saved. Default: `calendar.md`.
 252        
 253        Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`.
 254
 255        See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`.
 256        """
 257
 258        self.overviewFile = "overview.md"
 259        """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`.
 260
 261        See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`.
 262        """
 263
 264        self.overviewDigestFile = "overview-digest.md"
 265        """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`.
 266
 267        See also: `Overview()` with parameter `details="digest"`.
 268        """
 269
 270        self.overviewPositionsFile = "overview-positions.md"
 271        """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`.
 272
 273        See also: `Overview()` with parameter `details="positions"`.
 274        """
 275
 276        self.overviewOrdersFile = "overview-orders.md"
 277        """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`.
 278
 279        See also: `Overview()` with parameter `details="orders"`.
 280        """
 281
 282        self.overviewAnalyticsFile = "overview-analytics.md"
 283        """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`.
 284
 285        See also: `Overview()` with parameter `details="analytics"`.
 286        """
 287
 288        self.overviewBondsCalendarFile = "overview-calendar.md"
 289        """Filename where only the bonds calendar section will be saved. Default: `overview-calendar.md`.
 290
 291        See also: `Overview()` with parameter `details="calendar"`.
 292        """
 293
 294        self.reportFile = "deals.md"
 295        """Filename where history of deals and trade statistics will be saved. Default: `deals.md`.
 296
 297        See also: `Deals()`.
 298        """
 299
 300        self.withdrawalLimitsFile = "limits.md"
 301        """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`.
 302
 303        See also: `OverviewLimits()` and `RequestLimits()`.
 304        """
 305
 306        self.userInfoFile = "user-info.md"
 307        """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`.
 308
 309        See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`.
 310        """
 311
 312        self.userAccountsFile = "accounts.md"
 313        """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`.
 314
 315        See also: `OverviewAccounts()`, `RequestAccounts()`.
 316        """
 317
 318        self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache
 319        """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`.
 320
 321        Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`.
 322
 323        See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`.
 324        """
 325
 326        self.iList = None  # init iList for raw instruments data
 327        """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`.
 328        
 329        See also: `Listing()`, `DumpInstruments()`.
 330        """
 331
 332        # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server:
 333        if useCache:
 334            if os.path.exists(self.iListDumpFile):
 335                dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc())  # dump modification date and time
 336                curTime = datetime.now(tzutc())
 337
 338                if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year):
 339                    uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
 340
 341                    self.DumpInstruments(forceUpdate=True)  # updating self.iList and dump file
 342
 343                else:
 344                    self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8"))  # load iList from dump
 345
 346                    uLogger.debug("Local cache with raw instruments data is used: [{}]. Last modified: [{}] UTC".format(
 347                        os.path.abspath(self.iListDumpFile),
 348                        dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT),
 349                    ))
 350
 351            else:
 352                uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...")
 353                self.DumpInstruments(forceUpdate=True)  # updating self.iList and creating default dump file
 354
 355        else:
 356            self.iList = self.Listing()  # request new raw instruments data from broker server
 357            self.DumpInstruments(forceUpdate=False)  # save raw instrument's data to default dump file `iListDumpFile`
 358
 359        self.priceModel = PriceGenerator()  # init PriceGenerator object to work with candles data
 360        """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on.
 361
 362        See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator
 363        """
 364
 365    @property
 366    def tag(self) -> str:
 367        """Identification TKSBrokerAPI tag in log messages to simplify debugging when platform instances runs in parallel mode. Default: `""` (empty string)."""
 368        return self._tag
 369
 370    @tag.setter
 371    def tag(self, value):
 372        """Setter for Identification TKSBrokerAPI tag in log messages to simplify debugging when platform instances runs in parallel mode. Default: `""` (empty string)."""
 373        self._tag = str(value)
 374
 375        if self._tag:
 376            for handler in uLogger.handlers:
 377                handler.setFormatter(uLog.logging.Formatter(uLog.formatStringWithTag.format(tag=self._tag)))
 378
 379            uLogger.debug("Custom TKSBrokerAPI tag was set: {}".format(self._tag))
 380
 381        else:
 382            for handler in uLogger.handlers:
 383                handler.setFormatter(uLog.logging.Formatter(uLog.formatString))
 384
 385            uLogger.debug("Default logger format is used")
 386
 387    @property
 388    def ticker(self) -> str:
 389        """String with ticker, e.g. `GOOGL`. Tickers may be upper case only.
 390
 391        Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc.
 392        More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`.
 393
 394        See also: `SearchByTicker()`, `SearchInstruments()`.
 395        """
 396        return self._ticker
 397
 398    @ticker.setter
 399    def ticker(self, value):
 400        """Setter for string with ticker, e.g. `GOOGL`. Tickers may be upper case only.
 401
 402        Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc.
 403        More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`.
 404
 405        See also: `SearchByTicker()`, `SearchInstruments()`.
 406        """
 407        self._ticker = str(value).upper()  # Tickers may be upper case only
 408
 409    @property
 410    def figi(self) -> str:
 411        """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only.
 412
 413        See also: `SearchByFIGI()`, `SearchInstruments()`.
 414        """
 415        return self._figi
 416
 417    @figi.setter
 418    def figi(self, value):
 419        """Setter for string with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only.
 420
 421        See also: `SearchByFIGI()`, `SearchInstruments()`.
 422        """
 423        self._figi = str(value).upper()  # FIGI may be upper case only
 424
 425    def _ParseJSON(self, rawData="{}") -> dict:
 426        """
 427        Parse JSON from response string.
 428
 429        :param rawData: this is a string with JSON-formatted text.
 430        :return: JSON (dictionary), parsed from server response string. If an error occurred, then returns empty dict `{}`.
 431        """
 432        try:
 433            responseJSON = json.loads(rawData) if rawData else {}
 434
 435            if self.moreDebug:
 436                uLogger.debug("JSON formatted raw body data of response:\n{}".format(json.dumps(responseJSON, indent=4)))
 437
 438            return responseJSON
 439
 440        except Exception as e:
 441            uLogger.error("An empty dict will be return, because an error occurred in `_ParseJSON()` method with comment: {}".format(e))
 442
 443            return {}
 444
 445    def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5) -> dict:
 446        """
 447        Send GET or POST request to broker server and receive JSON object.
 448
 449        self.header: must be defining with dictionary of headers.
 450        self.body: if define then used as request body. None by default.
 451        self.timeout: global request timeout, 15 seconds by default.
 452        :param url: url with REST request.
 453        :param reqType: send "GET" or "POST" request. "GET" by default.
 454        :param retry: how many times retry after first request if an 5xx server errors occurred.
 455        :param pause: sleep time in seconds between retries.
 456        :return: response JSON (dictionary) from broker.
 457        """
 458        if reqType.upper() not in ("GET", "POST"):
 459            uLogger.error("You can define request type: `GET` or `POST`!")
 460            raise Exception("Incorrect value")
 461
 462        if self.moreDebug:
 463            uLogger.debug("Request parameters:")
 464            uLogger.debug("    - REST API URL: {}".format(url))
 465            uLogger.debug("    - request type: {}".format(reqType))
 466            uLogger.debug("    - headers:\n{}".format(str(self.headers).replace(self.token, "*** request token ***")))
 467            uLogger.debug("    - body:\n{}".format(self.body))
 468
 469        # fast hack to avoid all operations with some tickers/FIGI
 470        responseJSON = {}
 471        oK = True
 472        for item in self.exclude:
 473            if item in url:
 474                if self.moreDebug:
 475                    uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude)))
 476
 477                oK = False
 478                break
 479
 480        if oK:
 481            with self.__lock:  # acquire the mutex lock
 482                counter = 0
 483                response = None
 484                errMsg = ""
 485
 486                while not response and counter <= retry:
 487                    if reqType == "GET":
 488                        response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout)
 489
 490                    if reqType == "POST":
 491                        response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout)
 492
 493                    if self.moreDebug:
 494                        uLogger.debug("Response:")
 495                        uLogger.debug("    - status code: {}".format(response.status_code))
 496                        uLogger.debug("    - reason: {}".format(response.reason))
 497                        uLogger.debug("    - body length: {}".format(len(response.text)))
 498                        uLogger.debug("    - headers:\n{}".format(response.headers))
 499
 500                    # Server returns some headers:
 501                    # - `x-ratelimit-limit` — shows the settings of the current user limit for this method.
 502                    # - `x-ratelimit-remaining` — the number of remaining requests of this type per minute.
 503                    # - `x-ratelimit-reset` — time in seconds before resetting the request counter.
 504                    # See: https://tinkoff.github.io/investAPI/grpc/#kreya
 505                    if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0":
 506                        rateLimitWait = int(response.headers["x-ratelimit-reset"])
 507                        uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait))
 508                        sleep(rateLimitWait)
 509
 510                    # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes
 511                    if 400 <= response.status_code < 500:
 512                        msg = "status code: [{}], response body: {}".format(response.status_code, response.text)
 513                        uLogger.debug("    - not oK, but do not retry for 4xx errors, {}".format(msg))
 514
 515                        if "code" in response.text and "message" in response.text:
 516                            msgDict = self._ParseJSON(rawData=response.text)
 517                            uLogger.warning("HTTP-status code [{}], server message: {}".format(response.status_code, msgDict["message"]))
 518
 519                        counter = retry + 1  # do not retry for 4xx errors
 520
 521                    if 500 <= response.status_code < 600:
 522                        errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text)
 523                        uLogger.debug("    - not oK, {}".format(errMsg))
 524
 525                        if "code" in response.text and "message" in response.text:
 526                            errMsgDict = self._ParseJSON(rawData=response.text)
 527                            uLogger.warning("HTTP-status code [{}], error message: {}".format(response.status_code, errMsgDict["message"]))
 528
 529                        counter += 1
 530
 531                        if counter <= retry:
 532                            uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause))
 533                            sleep(pause)
 534
 535                responseJSON = self._ParseJSON(rawData=response.text)
 536
 537                if errMsg:
 538                    uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/")
 539                    uLogger.error("    - not oK, {}".format(errMsg))
 540
 541        return responseJSON
 542
 543    def _IUpdater(self, iType: str) -> tuple:
 544        """
 545        Request instrument by type from server. See available API methods for instruments:
 546        Currencies: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Currencies
 547        Shares: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Shares
 548        Bonds: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Bonds
 549        Etfs: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Etfs
 550        Futures: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Futures
 551
 552        :param iType: type of the instrument, it must be one of supported types in TKS_INSTRUMENTS list.
 553        :return: tuple with iType name and list of available instruments of current type for defined user token.
 554        """
 555        result = []
 556
 557        if iType in TKS_INSTRUMENTS:
 558            uLogger.debug("Requesting available [{}] list. Wait, please...".format(iType))
 559
 560            # all instruments have the same body in API v2 requests:
 561            self.body = str({"instrumentStatus": "INSTRUMENT_STATUS_UNSPECIFIED"})  # Enum: [INSTRUMENT_STATUS_UNSPECIFIED, INSTRUMENT_STATUS_BASE, INSTRUMENT_STATUS_ALL]
 562            instrumentURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/{}".format(iType)
 563            result = self.SendAPIRequest(instrumentURL, reqType="POST")["instruments"]
 564
 565        return iType, result
 566
 567    def _IWrapper(self, kwargs):
 568        """
 569        Wrapper runs instrument's update method `_IUpdater()`.
 570        It's a workaround for using multiprocessing with kwargs. See: https://stackoverflow.com/a/36799206
 571        """
 572        return self._IUpdater(**kwargs)
 573
 574    def Listing(self) -> dict:
 575        """
 576        Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server.
 577
 578        :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures.
 579        """
 580        uLogger.debug("Requesting all available instruments for current account. Wait, please...")
 581        uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES))
 582
 583        # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService
 584        # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list.
 585        iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS]
 586
 587        poolUpdater = ThreadPool(processes=CPU_USAGES)  # create pool for update instruments in parallel mode
 588        listing = poolUpdater.map(self._IWrapper, iParams)  # execute update operations
 589        poolUpdater.close()  # close the thread pool
 590        poolUpdater.join()  # wait a moment until all data returns from threads
 591
 592        # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures.
 593        # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method
 594        iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing}
 595
 596        # calculate minimum price increment (step) for all instruments and set up instrument's type:
 597        for iType in iList.keys():
 598            for ticker in iList[iType]:
 599                iList[iType][ticker]["type"] = iType
 600
 601                if "minPriceIncrement" in iList[iType][ticker].keys():
 602                    iList[iType][ticker]["step"] = NanoToFloat(
 603                        iList[iType][ticker]["minPriceIncrement"]["units"],
 604                        iList[iType][ticker]["minPriceIncrement"]["nano"],
 605                    )
 606
 607                else:
 608                    iList[iType][ticker]["step"] = 0  # hack to avoid empty value in some instruments, e.g. futures
 609
 610        return iList
 611
 612    def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None:
 613        """
 614        Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics.
 615
 616        See also: `DumpInstruments()`, `Listing()`.
 617
 618        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
 619                            otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) .
 620        """
 621        if self.iListDumpFile is None or not self.iListDumpFile:
 622            uLogger.error("Output name of dump file must be defined!")
 623            raise Exception("Filename required")
 624
 625        if not self.iList or forceUpdate:
 626            self.iList = self.Listing()
 627
 628        xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx"
 629
 630        # Save as XLSX with separated sheets for every type of instruments:
 631        with pd.ExcelWriter(
 632                path=xlsxDumpFile,
 633                date_format=TKS_DATE_FORMAT,
 634                datetime_format=TKS_DATE_TIME_FORMAT,
 635                mode="w",
 636        ) as writer:
 637            for iType in TKS_INSTRUMENTS:
 638                df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index")  # generate pandas object from self.iList dictionary
 639                df = df[sorted(df)]  # sorted by column names
 640                df = df.applymap(
 641                    lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item,
 642                    na_action="ignore",
 643                )  # converting numbers from nano-type to float in every cell
 644                df.to_excel(
 645                    writer,
 646                    sheet_name=iType,
 647                    encoding="UTF-8",
 648                    freeze_panes=(1, 1),
 649                )  # saving as XLSX-file with freeze first row and column as headers
 650
 651        uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile)))
 652
 653    def DumpInstruments(self, forceUpdate: bool = True) -> str:
 654        """
 655        Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server
 656        using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file.
 657
 658        See also: `DumpInstrumentsAsXLSX()`, `Listing()`.
 659
 660        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
 661                            otherwise just saves exist `iList` as JSON-file (default: `dump.json`).
 662        :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file.
 663        """
 664        if self.iListDumpFile is None or not self.iListDumpFile:
 665            uLogger.error("Output name of dump file must be defined!")
 666            raise Exception("Filename required")
 667
 668        if not self.iList or forceUpdate:
 669            self.iList = self.Listing()
 670
 671        jsonDump = json.dumps(self.iList, indent=4, sort_keys=False)  # create JSON object as string
 672        with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH:
 673            fH.write(jsonDump)
 674
 675        uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile)))
 676
 677        return jsonDump
 678
 679    def ShowInstrumentInfo(self, iJSON: dict, show: bool = True, onlyFiles=False) -> str:
 680        """
 681        Show information about one instrument defined by json data and prints it in Markdown format.
 682
 683        See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`.
 684
 685        :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self._ticker]`
 686        :param show: if `True` then also printing information about instrument and its current price.
 687        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
 688        :return: multilines text in Markdown format with information about one instrument.
 689        """
 690        splitLine = "|                                                             |                                                        |\n"
 691        infoText = ""
 692
 693        if iJSON is not None and iJSON and isinstance(iJSON, dict):
 694            info = [
 695                "# Main information\n\n",
 696                "* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
 697                "| Parameters                                                  | Values                                                 |\n",
 698                "|-------------------------------------------------------------|--------------------------------------------------------|\n",
 699                "| Ticker:                                                     | {:<54} |\n".format(iJSON["ticker"]),
 700                "| Full name:                                                  | {:<54} |\n".format(iJSON["name"]),
 701            ]
 702
 703            if "sector" in iJSON.keys() and iJSON["sector"]:
 704                info.append("| Sector:                                                     | {:<54} |\n".format(iJSON["sector"]))
 705
 706            if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] and "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"]:
 707                info.append("| Country of instrument:                                      | {:<54} |\n".format("({}) {}".format(iJSON["countryOfRisk"], iJSON["countryOfRiskName"])))
 708
 709            info.extend([
 710                splitLine,
 711                "| FIGI (Financial Instrument Global Identifier):              | {:<54} |\n".format(iJSON["figi"]),
 712                "| Real exchange [Exchange section]:                           | {:<54} |\n".format("{} [{}]".format(TKS_REAL_EXCHANGES[iJSON["realExchange"]], iJSON["exchange"])),
 713            ])
 714
 715            if "isin" in iJSON.keys() and iJSON["isin"]:
 716                info.append("| ISIN (International Securities Identification Number):      | {:<54} |\n".format(iJSON["isin"]))
 717
 718            if "classCode" in iJSON.keys():
 719                info.append("| Class Code (exchange section where instrument is traded):   | {:<54} |\n".format(iJSON["classCode"]))
 720
 721            info.extend([
 722                splitLine,
 723                "| Current broker security trading status:                     | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]),
 724                splitLine,
 725                "| Buy operations allowed:                                     | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"),
 726                "| Sale operations allowed:                                    | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"),
 727                "| Short positions allowed:                                    | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"),
 728            ])
 729
 730            if iJSON["figi"]:
 731                self._figi = iJSON["figi"]
 732                iJSON = iJSON | self.RequestTradingStatus()
 733
 734                info.extend([
 735                    splitLine,
 736                    "| Limit orders allowed:                                       | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"),
 737                    "| Market orders allowed:                                      | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"),
 738                    "| API trade allowed:                                          | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"),
 739                ])
 740
 741            info.append(splitLine)
 742
 743            if "type" in iJSON.keys() and iJSON["type"]:
 744                info.append("| Type of the instrument:                                     | {:<54} |\n".format(iJSON["type"]))
 745
 746                if "shareType" in iJSON.keys() and iJSON["shareType"]:
 747                    info.append("| Share type:                                                 | {:<54} |\n".format(TKS_SHARE_TYPES[iJSON["shareType"]]))
 748
 749            if "futuresType" in iJSON.keys() and iJSON["futuresType"]:
 750                info.append("| Futures type:                                               | {:<54} |\n".format(iJSON["futuresType"]))
 751
 752            if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]:
 753                info.append("| IPO date:                                                   | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", "")))
 754
 755            if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]:
 756                info.append("| Released date:                                              | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", "")))
 757
 758            if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]:
 759                info.append("| Rebalancing frequency:                                      | {:<54} |\n".format(iJSON["rebalancingFreq"]))
 760
 761            if "focusType" in iJSON.keys() and iJSON["focusType"]:
 762                info.append("| Focusing type:                                              | {:<54} |\n".format(iJSON["focusType"]))
 763
 764            if "assetType" in iJSON.keys() and iJSON["assetType"]:
 765                info.append("| Asset type:                                                 | {:<54} |\n".format(iJSON["assetType"]))
 766
 767            if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]:
 768                info.append("| Basic asset:                                                | {:<54} |\n".format(iJSON["basicAsset"]))
 769
 770            if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]:
 771                info.append("| Basic asset size:                                           | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"]))))
 772
 773            if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]:
 774                info.append("| ISO currency name:                                          | {:<54} |\n".format(iJSON["isoCurrencyName"]))
 775
 776            if "currency" in iJSON.keys():
 777                info.append("| Payment currency:                                           | {:<54} |\n".format(iJSON["currency"]))
 778
 779            if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys():
 780                info.append("| Nominal currency:                                           | {:<54} |\n".format(iJSON["nominal"]["currency"]))
 781
 782            if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]:
 783                info.append("| First trade date:                                           | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", "")))
 784
 785            if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]:
 786                info.append("| Last trade date:                                            | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", "")))
 787
 788            if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]:
 789                info.append("| Date of expiration:                                         | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", "")))
 790
 791            if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]:
 792                info.append("| State registration date:                                    | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", "")))
 793
 794            if "placementDate" in iJSON.keys() and iJSON["placementDate"]:
 795                info.append("| Placement date:                                             | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", "")))
 796
 797            if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]:
 798                info.append("| Maturity date:                                              | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", "")))
 799
 800            if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]:
 801                info.append("| Perpetual bond:                                             | Yes                                                    |\n")
 802
 803            if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]:
 804                info.append("| Over-the-counter (OTC) securities:                          | Yes                                                    |\n")
 805
 806            iExt = None
 807            if iJSON["type"] == "Bonds":
 808                info.extend([
 809                    splitLine,
 810                    "| Bond issue (size / plan):                                   | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])),
 811                    "| Nominal price (100%):                                       | {:<54} |\n".format("{} {}".format(
 812                        "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."),
 813                        iJSON["nominal"]["currency"],
 814                    )),
 815                ])
 816
 817                if "floatingCouponFlag" in iJSON.keys():
 818                    info.append("| Floating coupon:                                            | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No"))
 819
 820                if "amortizationFlag" in iJSON.keys():
 821                    info.append("| Amortization:                                               | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No"))
 822
 823                info.append(splitLine)
 824
 825                if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]:
 826                    info.append("| Number of coupon payments per year:                         | {:<54} |\n".format(iJSON["couponQuantityPerYear"]))
 827
 828                if iJSON["figi"]:
 829                    iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False)  # extended bonds data
 830
 831                    info.extend([
 832                        "| Days last to maturity date:                                 | {:<54} |\n".format(iExt["daysToMaturity"][0]),
 833                        "| Coupons yield (average coupon daily yield * 365):           | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])),
 834                        "| Current price yield (average daily yield * 365):            | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])),
 835                    ])
 836
 837                if "aciValue" in iJSON.keys() and iJSON["aciValue"]:
 838                    info.append("| Current accumulated coupon income (ACI):                    | {:<54} |\n".format("{:.2f} {}".format(
 839                        NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]),
 840                        iJSON["aciValue"]["currency"]
 841                    )))
 842
 843            if "currentPrice" in iJSON.keys():
 844                info.append(splitLine)
 845
 846                currency = iJSON["currency"] if "currency" in iJSON.keys() else ""  # nominal currency for bonds, otherwise currency of instrument
 847                aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else ""  # payment currency
 848
 849                bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0  # previous close price of bond
 850                bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0  # last price of bond
 851                bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0  # max price of bond
 852                bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0  # min price of bond
 853                bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0  # delta between last deal price and last close
 854
 855                curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0
 856                curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0
 857
 858                info.extend([
 859                    "| Previous close price of the instrument:                     | {:<54} |\n".format("{}{}".format(
 860                        "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A",
 861                        "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
 862                    )),
 863                    "| Last deal price of the instrument:                          | {:<54} |\n".format("{}{}".format(
 864                        "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A",
 865                        "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
 866                    )),
 867                    "| Changes between last deal price and last close              | {:<54} |\n".format(
 868                        "{:.2f}%{}".format(
 869                            iJSON["currentPrice"]["changes"],
 870                            " ({}{:.2f} {})".format(
 871                                "+" if bondChangesDelta > 0 else "",
 872                                bondChangesDelta,
 873                                aciCurrency
 874                            ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format(
 875                                "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "",
 876                                iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"],
 877                                currency
 878                            ),
 879                        )
 880                    ),
 881                    "| Current limit price, min / max:                             | {:<54} |\n".format("{}{} / {}{}{}".format(
 882                        "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A",
 883                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 884                        "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A",
 885                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 886                        " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else ""
 887                    )),
 888                    "| Actual price, sell / buy:                                   | {:<54} |\n".format("{}{} / {}{}{}".format(
 889                        "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A",
 890                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 891                        "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A",
 892                        "%" if iJSON["type"] == "Bonds" else" {}".format(currency),
 893                        " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else ""
 894                    )),
 895                ])
 896
 897            if "lot" in iJSON.keys():
 898                info.append("| Minimum lot to buy:                                         | {:<54} |\n".format(iJSON["lot"]))
 899
 900            if "step" in iJSON.keys() and iJSON["step"] != 0:
 901                info.append("| Minimum price increment (step):                             | {:<54} |\n".format("{} {}".format(iJSON["step"], iJSON["currency"] if "currency" in iJSON.keys() else "")))
 902
 903            # Add bond payment calendar:
 904            if iJSON["type"] == "Bonds":
 905                strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False)   # bond payment calendar
 906                info.extend(["\n#", strCalendar])
 907
 908            infoText += "".join(info)
 909
 910            if show and not onlyFiles:
 911                uLogger.info("{}".format(infoText))
 912
 913            if self.infoFile is not None and (show or onlyFiles):
 914                with open(self.infoFile, "w", encoding="UTF-8") as fH:
 915                    fH.write(infoText)
 916
 917                uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile)))
 918
 919                if self.useHTMLReports:
 920                    htmlFilePath = self.infoFile.replace(".md", ".html") if self.infoFile.endswith(".md") else self.infoFile + ".html"
 921                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
 922                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Main information", commonCSS=COMMON_CSS, markdown=infoText))
 923
 924                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
 925
 926        return infoText
 927
 928    def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict:
 929        """
 930        Search and return raw broker's information about instrument by its ticker. Variable `ticker` must be defined!
 931
 932        :param requestPrice: if `False` then do not request current price of instrument (because this is long operation).
 933        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
 934        :return: JSON formatted data with information about instrument.
 935        """
 936        tickerJSON = {}
 937        if self.moreDebug:
 938            uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self._ticker))
 939
 940        if not self._ticker:
 941            uLogger.warning("self._ticker variable is not be empty!")
 942
 943        else:
 944            if self._ticker in TKS_TICKERS_OR_FIGI_EXCLUDED:
 945                uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self._ticker))
 946                raise Exception("Instrument not allowed")
 947
 948            if not self.iList:
 949                self.iList = self.Listing()
 950
 951            if self._ticker in self.iList["Shares"].keys():
 952                tickerJSON = self.iList["Shares"][self._ticker]
 953                if self.moreDebug:
 954                    uLogger.debug("Ticker [{}] found in shares list".format(self._ticker))
 955
 956            elif self._ticker in self.iList["Currencies"].keys():
 957                tickerJSON = self.iList["Currencies"][self._ticker]
 958                if self.moreDebug:
 959                    uLogger.debug("Ticker [{}] found in currencies list".format(self._ticker))
 960
 961            elif self._ticker in self.iList["Bonds"].keys():
 962                tickerJSON = self.iList["Bonds"][self._ticker]
 963                if self.moreDebug:
 964                    uLogger.debug("Ticker [{}] found in bonds list".format(self._ticker))
 965
 966            elif self._ticker in self.iList["Etfs"].keys():
 967                tickerJSON = self.iList["Etfs"][self._ticker]
 968                if self.moreDebug:
 969                    uLogger.debug("Ticker [{}] found in etfs list".format(self._ticker))
 970
 971            elif self._ticker in self.iList["Futures"].keys():
 972                tickerJSON = self.iList["Futures"][self._ticker]
 973                if self.moreDebug:
 974                    uLogger.debug("Ticker [{}] found in futures list".format(self._ticker))
 975
 976        if tickerJSON:
 977            self._figi = tickerJSON["figi"]
 978
 979            if requestPrice:
 980                tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False)
 981
 982                if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None:
 983                    tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"]
 984
 985                else:
 986                    tickerJSON["currentPrice"]["changes"] = 0
 987
 988            if show:
 989                self.ShowInstrumentInfo(iJSON=tickerJSON, show=True)  # print info as Markdown text
 990
 991        else:
 992            if show:
 993                uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self._ticker))
 994
 995        return tickerJSON
 996
 997    def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict:
 998        """
 999        Search and return raw broker's information about instrument by its FIGI. Variable `figi` must be defined!
1000
1001        :param requestPrice: if `False` then do not request current price of instrument (it's long operation).
1002        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
1003        :return: JSON formatted data with information about instrument.
1004        """
1005        figiJSON = {}
1006        if self.moreDebug:
1007            uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self._figi))
1008
1009        if not self._figi:
1010            uLogger.warning("self._figi variable is not be empty!")
1011
1012        else:
1013            if self._figi in TKS_TICKERS_OR_FIGI_EXCLUDED:
1014                uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self._figi))
1015                raise Exception("Instrument not allowed")
1016
1017            if not self.iList:
1018                self.iList = self.Listing()
1019
1020            for item in self.iList["Shares"].keys():
1021                if self._figi == self.iList["Shares"][item]["figi"]:
1022                    figiJSON = self.iList["Shares"][item]
1023
1024                    if self.moreDebug:
1025                        uLogger.debug("FIGI [{}] found in shares list".format(self._figi))
1026
1027                    break
1028
1029            if not figiJSON:
1030                for item in self.iList["Currencies"].keys():
1031                    if self._figi == self.iList["Currencies"][item]["figi"]:
1032                        figiJSON = self.iList["Currencies"][item]
1033
1034                        if self.moreDebug:
1035                            uLogger.debug("FIGI [{}] found in currencies list".format(self._figi))
1036
1037                        break
1038
1039            if not figiJSON:
1040                for item in self.iList["Bonds"].keys():
1041                    if self._figi == self.iList["Bonds"][item]["figi"]:
1042                        figiJSON = self.iList["Bonds"][item]
1043
1044                        if self.moreDebug:
1045                            uLogger.debug("FIGI [{}] found in bonds list".format(self._figi))
1046
1047                        break
1048
1049            if not figiJSON:
1050                for item in self.iList["Etfs"].keys():
1051                    if self._figi == self.iList["Etfs"][item]["figi"]:
1052                        figiJSON = self.iList["Etfs"][item]
1053
1054                        if self.moreDebug:
1055                            uLogger.debug("FIGI [{}] found in etfs list".format(self._figi))
1056
1057                        break
1058
1059            if not figiJSON:
1060                for item in self.iList["Futures"].keys():
1061                    if self._figi == self.iList["Futures"][item]["figi"]:
1062                        figiJSON = self.iList["Futures"][item]
1063
1064                        if self.moreDebug:
1065                            uLogger.debug("FIGI [{}] found in futures list".format(self._figi))
1066
1067                        break
1068
1069        if figiJSON:
1070            self._figi = figiJSON["figi"]
1071            self._ticker = figiJSON["ticker"]
1072
1073            if requestPrice:
1074                figiJSON["currentPrice"] = self.GetCurrentPrices(show=False)
1075
1076                if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None:
1077                    figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"]
1078
1079                else:
1080                    figiJSON["currentPrice"]["changes"] = 0
1081
1082            if show:
1083                self.ShowInstrumentInfo(iJSON=figiJSON, show=True)  # print info as Markdown text
1084
1085        else:
1086            if show:
1087                uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self._figi))
1088
1089        return figiJSON
1090
1091    def GetCurrentPrices(self, show: bool = True) -> dict:
1092        """
1093        Get and show Depth of Market with current prices of the instrument as dictionary. Result example with `depth` 5:
1094        `{"buy": [{"price": 1243.8, "quantity": 193},
1095                  {"price": 1244.0, "quantity": 168},
1096                  {"price": 1244.8, "quantity": 5},
1097                  {"price": 1245.0, "quantity": 61},
1098                  {"price": 1245.4, "quantity": 60}],
1099          "sell": [{"price": 1243.6, "quantity": 8},
1100                   {"price": 1242.6, "quantity": 10},
1101                   {"price": 1242.4, "quantity": 18},
1102                   {"price": 1242.2, "quantity": 50},
1103                   {"price": 1242.0, "quantity": 113}],
1104          "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}`, where parameters mean:
1105        - buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order
1106        - sell: list of dicts with Buyers prices,
1107            - price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument),
1108            - quantity: volume value by current price in lots,
1109        - limitUp: current trade session limit price, maximum,
1110        - limitDown: current trade session limit price, minimum,
1111        - lastPrice: last deal price of the instrument,
1112        - closePrice: previous trade session close price of the instrument.
1113
1114        See also: `SearchByTicker()` and `SearchByFIGI()`.
1115        REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1116        Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
1117
1118        :param show: if `True` then print DOM to log and console.
1119        :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`.
1120                 If an error occurred then returns an empty record:
1121                 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`.
1122        """
1123        prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0}
1124
1125        if self.depth < 1:
1126            uLogger.error("Depth of Market (DOM) must be >=1!")
1127            raise Exception("Incorrect value")
1128
1129        if not (self._ticker or self._figi):
1130            uLogger.error("self._ticker or self._figi variables must be defined!")
1131            raise Exception("Ticker or FIGI required")
1132
1133        if self._ticker and not self._figi:
1134            instrumentByTicker = self.SearchByTicker(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1135            self._figi = instrumentByTicker["figi"] if instrumentByTicker else ""
1136
1137        if not self._ticker and self._figi:
1138            instrumentByFigi = self.SearchByFIGI(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1139            self._ticker = instrumentByFigi["ticker"] if instrumentByFigi else ""
1140
1141        if not self._figi:
1142            uLogger.error("FIGI is not defined!")
1143            raise Exception("Ticker or FIGI required")
1144
1145        else:
1146            uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self._ticker, self._figi))
1147
1148            # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1149            priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook"
1150            self.body = str({"figi": self._figi, "depth": self.depth})
1151            pricesResponse = self.SendAPIRequest(priceURL, reqType="POST")  # Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
1152
1153            if pricesResponse and not ("code" in pricesResponse.keys() or "message" in pricesResponse.keys() or "description" in pricesResponse.keys()):
1154                # list of dicts with sellers orders:
1155                prices["buy"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]]
1156
1157                # list of dicts with buyers orders:
1158                prices["sell"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]]
1159
1160                # max price of instrument at this time:
1161                prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None
1162
1163                # min price of instrument at this time:
1164                prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None
1165
1166                # last price of deal with instrument:
1167                prices["lastPrice"] = round(NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]), 6) if "lastPrice" in pricesResponse.keys() else 0
1168
1169                # last close price of instrument:
1170                prices["closePrice"] = round(NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]), 6) if "closePrice" in pricesResponse.keys() else 0
1171
1172            else:
1173                uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi))
1174                uLogger.debug("Server response: {}".format(pricesResponse))
1175
1176            if show:
1177                if prices["buy"] or prices["sell"]:
1178                    info = [
1179                        "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format(
1180                            datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
1181                            self._ticker,
1182                            self._figi,
1183                            self.depth,
1184                        ),
1185                        "-" * 60, "\n",
1186                        "             Orders of Buyers | Orders of Sellers\n",
1187                        "-" * 60, "\n",
1188                        "        Sell prices (volumes) | Buy prices (volumes)\n",
1189                        "-" * 60, "\n",
1190                    ]
1191
1192                    if not prices["buy"]:
1193                        info.append("                              | No orders!\n")
1194                        sumBuy = 0
1195
1196                    else:
1197                        sumBuy = sum([x["quantity"] for x in prices["buy"]])
1198                        maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True)
1199                        for item in maxMinSorted:
1200                            info.append("                              | {} ({})\n".format(item["price"], item["quantity"]))
1201
1202                    if not prices["sell"]:
1203                        info.append("No orders!                    |\n")
1204                        sumSell = 0
1205
1206                    else:
1207                        sumSell = sum([x["quantity"] for x in prices["sell"]])
1208                        for item in prices["sell"]:
1209                            info.append("{:>29} |\n".format("{} ({})".format(item["price"], item["quantity"])))
1210
1211                    info.extend([
1212                        "-" * 60, "\n",
1213                        "{:>29} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)),
1214                        "-" * 60, "\n",
1215                    ])
1216
1217                    infoText = "".join(info)
1218
1219                    uLogger.info("Current prices in order book:\n\n{}".format(infoText))
1220
1221                else:
1222                    uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi))
1223
1224        return prices
1225
1226    def ShowInstrumentsInfo(self, show: bool = True, onlyFiles=False) -> str:
1227        """
1228        This method get and show information about all available broker instruments for current user account.
1229        If `instrumentsFile` string is not empty then also save information to this file.
1230
1231        :param show: if `True` then print results to console, if `False` — print only to file.
1232        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
1233        :return: multi-lines string with all available broker instruments.
1234        """
1235        if not self.iList:
1236            self.iList = self.Listing()
1237
1238        info = [
1239            "# All available instruments from Tinkoff Broker server for current user token\n\n",
1240            "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
1241        ]
1242
1243        # add instruments count by type:
1244        for iType in self.iList.keys():
1245            info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType])))
1246
1247        headerLine = "| Ticker       | Full name                                                 | FIGI         | Cur | Lot     | Step       |\n"
1248        splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n"
1249
1250        # generating info tables with all instruments by type:
1251        for iType in self.iList.keys():
1252            info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine])
1253
1254            for instrument in self.iList[iType].keys():
1255                iName = self.iList[iType][instrument]["name"]  # instrument's name
1256                if len(iName) > 57:
1257                    iName = "{}...".format(iName[:54])  # right trim for a long string
1258
1259                info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format(
1260                    self.iList[iType][instrument]["ticker"],
1261                    iName,
1262                    self.iList[iType][instrument]["figi"],
1263                    self.iList[iType][instrument]["currency"],
1264                    self.iList[iType][instrument]["lot"],
1265                    "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0,
1266                ))
1267
1268        infoText = "".join(info)
1269
1270        if show and not onlyFiles:
1271            uLogger.info(infoText)
1272
1273        if self.instrumentsFile and (show or onlyFiles):
1274            with open(self.instrumentsFile, "w", encoding="UTF-8") as fH:
1275                fH.write(infoText)
1276
1277            uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile)))
1278
1279            if self.useHTMLReports:
1280                htmlFilePath = self.instrumentsFile.replace(".md", ".html") if self.instrumentsFile.endswith(".md") else self.instrumentsFile + ".html"
1281                with open(htmlFilePath, "w", encoding="UTF-8") as fH:
1282                    fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="List of instruments", commonCSS=COMMON_CSS, markdown=infoText))
1283
1284                uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
1285
1286        return infoText
1287
1288    def SearchInstruments(self, pattern: str, show: bool = True, onlyFiles=False) -> dict:
1289        """
1290        This method search and show information about instruments by part of its ticker, FIGI or name.
1291        If `searchResultsFile` string is not empty then also save information to this file.
1292
1293        :param pattern: string with part of ticker, FIGI or instrument's name.
1294        :param show: if `True` then print results to console, if `False` — return list of result only.
1295        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
1296        :return: list of dictionaries with all found instruments.
1297        """
1298        if not self.iList:
1299            self.iList = self.Listing()
1300
1301        searchResults = {iType: {} for iType in self.iList}  # same as iList but will contain only filtered instruments
1302        compiledPattern = re.compile(pattern, re.IGNORECASE)
1303
1304        for iType in self.iList:
1305            for instrument in self.iList[iType].values():
1306                searchResult = compiledPattern.search(" ".join(
1307                    [instrument["ticker"], instrument["figi"], instrument["name"]]
1308                ))
1309
1310                if searchResult:
1311                    searchResults[iType][instrument["ticker"]] = instrument
1312
1313        resultsLen = sum([len(searchResults[iType]) for iType in searchResults])
1314        info = [
1315            "# Search results\n\n",
1316            "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
1317            "* **Search pattern:** [{}]\n".format(pattern),
1318            "* **Found instruments:** [{}]\n\n".format(resultsLen),
1319            '**Note:** you can view info about found instruments with key "--info", e.g.: "tksbrokerapi -t TICKER --info" or "tksbrokerapi -f FIGI --info".\n'
1320        ]
1321        infoShort = info[:]
1322
1323        headerLine = "| Type       | Ticker       | Full name                                                      | FIGI         |\n"
1324        splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n"
1325        skippedLine = "| ...        | ...          | ...                                                            | ...          |\n"
1326
1327        if resultsLen == 0:
1328            info.append("\nNo results\n")
1329            infoShort.append("\nNo results\n")
1330            uLogger.warning("No results. Try changing your search pattern.")
1331
1332        else:
1333            for iType in searchResults:
1334                iTypeValuesCount = len(searchResults[iType].values())
1335                if iTypeValuesCount > 0:
1336                    info.extend(["\n## {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1337                    infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1338
1339                    for instrument in searchResults[iType].values():
1340                        info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format(
1341                            instrument["type"],
1342                            instrument["ticker"],
1343                            "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"],  # right trim for a long string
1344                            instrument["figi"],
1345                        ))
1346
1347                    if iTypeValuesCount <= 5:
1348                        infoShort.extend(info[-iTypeValuesCount:])
1349
1350                    else:
1351                        infoShort.extend(info[-5:])
1352                        infoShort.append(skippedLine)
1353
1354        infoText = "".join(info)
1355        infoTextShort = "".join(infoShort)
1356
1357        if show and not onlyFiles:
1358            uLogger.info(infoTextShort)
1359            uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`")
1360
1361        if self.searchResultsFile and (show or onlyFiles):
1362            with open(self.searchResultsFile, "w", encoding="UTF-8") as fH:
1363                fH.write(infoText)
1364
1365            uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile)))
1366
1367            if self.useHTMLReports:
1368                htmlFilePath = self.searchResultsFile.replace(".md", ".html") if self.searchResultsFile.endswith(".md") else self.searchResultsFile + ".html"
1369                with open(htmlFilePath, "w", encoding="UTF-8") as fH:
1370                    fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Search results", commonCSS=COMMON_CSS, markdown=infoText))
1371
1372                uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
1373
1374        return searchResults
1375
1376    def GetUniqueFIGIs(self, instruments: list[str]) -> list:
1377        """
1378        Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs.
1379
1380        :param instruments: list of strings with tickers or FIGIs.
1381        :return: list with unique instrument FIGIs only.
1382        """
1383        requestedInstruments = []
1384        for iName in instruments:
1385            if iName not in self.aliases.keys():
1386                if iName not in requestedInstruments:
1387                    requestedInstruments.append(iName)
1388
1389            else:
1390                if iName not in requestedInstruments:
1391                    if self.aliases[iName] not in requestedInstruments:
1392                        requestedInstruments.append(self.aliases[iName])
1393
1394        uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments))
1395
1396        onlyUniqueFIGIs = []
1397        for iName in requestedInstruments:
1398            if iName in TKS_TICKERS_OR_FIGI_EXCLUDED:
1399                continue
1400
1401            self._ticker = iName
1402            iData = self.SearchByTicker(requestPrice=False)  # trying to find instrument by ticker
1403
1404            if not iData:
1405                self._ticker = ""
1406                self._figi = iName
1407
1408                iData = self.SearchByFIGI(requestPrice=False)  # trying to find instrument by FIGI
1409
1410                if not iData:
1411                    self._figi = ""
1412                    uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName))
1413
1414            if iData and iData["figi"] not in onlyUniqueFIGIs:
1415                onlyUniqueFIGIs.append(iData["figi"])
1416
1417        uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs))
1418
1419        return onlyUniqueFIGIs
1420
1421    def GetListOfPrices(self, instruments: list[str], show: bool = False, onlyFiles=False) -> list[dict]:
1422        """
1423        This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation!
1424
1425        See limits: https://tinkoff.github.io/investAPI/limits/
1426
1427        If `pricesFile` string is not empty then also save information to this file.
1428
1429        :param instruments: list of strings with tickers or FIGIs.
1430        :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`.
1431        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
1432        :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1433                 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods.
1434        """
1435        if instruments is None or not instruments:
1436            uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!")
1437            raise Exception("Ticker or FIGI required")
1438
1439        onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments)
1440
1441        uLogger.debug("Requesting current prices from Tinkoff Broker server...")
1442
1443        iList = []  # trying to get info and current prices about all unique instruments:
1444        for self._figi in onlyUniqueFIGIs:
1445            iData = self.SearchByFIGI(requestPrice=True, show=False)
1446            iList.append(iData)
1447
1448        self.ShowListOfPrices(iList, show, onlyFiles)
1449
1450        return iList
1451
1452    def ShowListOfPrices(self, iList: list, show: bool = True, onlyFiles=False) -> str:
1453        """
1454        Show table contains current prices of given instruments.
1455
1456        :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1457                      One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods.
1458        :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`.
1459        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
1460        :return: multilines text in Markdown format as a table contains current prices.
1461        """
1462        infoText = ""
1463
1464        if show or self.pricesFile or onlyFiles:
1465            info = [
1466                "# Current prices\n\n* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
1467                "| Ticker       | FIGI         | Type       | Prev. close | Last price  | Chg. %   | Day limits min/max  | Actual sell / buy   | Curr. |\n",
1468                "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n",
1469            ]
1470
1471            for item in iList:
1472                info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format(
1473                    item["ticker"],
1474                    item["figi"],
1475                    item["type"],
1476                    "{:.2f}".format(float(item["currentPrice"]["closePrice"])),
1477                    "{:.2f}".format(float(item["currentPrice"]["lastPrice"])),
1478                    "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])),
1479                    "{} / {}".format(
1480                        item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A",
1481                        item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A",
1482                    ),
1483                    "{} / {}".format(
1484                        item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A",
1485                        item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A",
1486                    ),
1487                    item["currency"],
1488                ))
1489
1490            infoText = "".join(info)
1491
1492            if show and not onlyFiles:
1493                uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText))
1494
1495            if self.pricesFile and (show or onlyFiles):
1496                with open(self.pricesFile, "w", encoding="UTF-8") as fH:
1497                    fH.write(infoText)
1498
1499                uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile)))
1500
1501                if self.useHTMLReports:
1502                    htmlFilePath = self.pricesFile.replace(".md", ".html") if self.pricesFile.endswith(".md") else self.pricesFile + ".html"
1503                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
1504                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Current prices", commonCSS=COMMON_CSS, markdown=infoText))
1505
1506                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
1507
1508        return infoText
1509
1510    def RequestTradingStatus(self) -> dict:
1511        """
1512        Requesting trading status for the instrument defined by `figi` variable.
1513
1514        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus
1515
1516        Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest
1517
1518        :return: dictionary with trading status attributes. Response example:
1519                 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING",
1520                  "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}`
1521        """
1522        if self._figi is None or not self._figi:
1523            uLogger.error("Variable `figi` must be defined for using this method!")
1524            raise Exception("FIGI required")
1525
1526        uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self._figi))
1527
1528        self.body = str({"figi": self._figi, "instrumentId": self._figi})
1529        tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus"
1530        tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST")
1531
1532        if self.moreDebug:
1533            uLogger.debug("Records about current trading status successfully received")
1534
1535        return tradingStatus
1536
1537    def RequestPortfolio(self) -> dict:
1538        """
1539        Requesting actual user's portfolio for current `accountId`.
1540
1541        REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio
1542
1543        Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest
1544
1545        :return: dictionary with user's portfolio.
1546        """
1547        if self.accountId is None or not self.accountId:
1548            uLogger.error("Variable `accountId` must be defined for using this method!")
1549            raise Exception("Account ID required")
1550
1551        uLogger.debug("Requesting current actual user's portfolio. Wait, please...")
1552
1553        self.body = str({"accountId": self.accountId})
1554        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio"
1555        rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST")
1556
1557        if self.moreDebug:
1558            uLogger.debug("Records about user's portfolio successfully received")
1559
1560        return rawPortfolio
1561
1562    def RequestPositions(self) -> dict:
1563        """
1564        Requesting open positions by currencies and instruments for current `accountId`.
1565
1566        REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions
1567
1568        Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest
1569
1570        :return: dictionary with open positions by instruments.
1571        """
1572        if self.accountId is None or not self.accountId:
1573            uLogger.error("Variable `accountId` must be defined for using this method!")
1574            raise Exception("Account ID required")
1575
1576        uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...")
1577
1578        self.body = str({"accountId": self.accountId})
1579        positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions"
1580        rawPositions = self.SendAPIRequest(positionsURL, reqType="POST")
1581
1582        if self.moreDebug:
1583            uLogger.debug("Records about current open positions successfully received")
1584
1585        return rawPositions
1586
1587    def RequestPendingOrders(self) -> list:
1588        """
1589        Requesting current actual pending limit orders for current `accountId`.
1590
1591        REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders
1592
1593        Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest
1594
1595        :return: list of dictionaries with pending limit orders.
1596        """
1597        if self.accountId is None or not self.accountId:
1598            uLogger.error("Variable `accountId` must be defined for using this method!")
1599            raise Exception("Account ID required")
1600
1601        uLogger.debug("Requesting current actual pending limit orders. Wait, please...")
1602
1603        self.body = str({"accountId": self.accountId})
1604        ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders"
1605        rawResponse = self.SendAPIRequest(ordersURL, reqType="POST")
1606
1607        if "orders" in rawResponse.keys():
1608            rawOrders = rawResponse["orders"]
1609            uLogger.debug("[{}] records about pending limit orders received".format(len(rawOrders)))
1610
1611        else:
1612            rawOrders = []
1613            uLogger.debug("No pending limit orders returned! rawResponse = {}".format(rawResponse))
1614
1615        return rawOrders
1616
1617    def RequestStopOrders(self) -> list:
1618        """
1619        Requesting current actual stop orders for current `accountId`.
1620
1621        REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders
1622
1623        Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest
1624
1625        :return: list of dictionaries with stop orders.
1626        """
1627        if self.accountId is None or not self.accountId:
1628            uLogger.error("Variable `accountId` must be defined for using this method!")
1629            raise Exception("Account ID required")
1630
1631        uLogger.debug("Requesting current actual stop orders. Wait, please...")
1632
1633        self.body = str({"accountId": self.accountId})
1634        stopOrdersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders"
1635        rawResponse = self.SendAPIRequest(stopOrdersURL, reqType="POST")
1636
1637        if "stopOrders" in rawResponse.keys():
1638            rawStopOrders = rawResponse["stopOrders"]
1639            uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders)))
1640
1641        else:
1642            rawStopOrders = []
1643            uLogger.debug("No stop orders returned! rawResponse = {}".format(rawResponse))
1644
1645        return rawStopOrders
1646
1647    def Overview(self, show: bool = False, details: str = "full", onlyFiles=False) -> dict:
1648        """
1649        Get portfolio: all open positions, orders and some statistics for current `accountId`.
1650        If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile`
1651        and `overviewBondsCalendarFile` are defined then also save information to file.
1652
1653        WARNING! It is not recommended to run this method too many times in a loop! The server receives
1654        many requests about the state of the portfolio, and then, based on the received data, a large number
1655        of calculation and statistics are collected.
1656
1657        :param show: if `False` then only dictionary returns, if `True` then show more debug information.
1658        :param details: how detailed should the information be?
1659        - `full` — shows full available information about portfolio status (by default),
1660        - `positions` — shows only open positions,
1661        - `orders` — shows only sections of open limits and stop orders.
1662        - `digest` — show a short digest of the portfolio status,
1663        - `analytics` — shows only the analytics section and the distribution of the portfolio by various categories,
1664        - `calendar` — shows only the bonds calendar section (if these present in portfolio).
1665        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
1666        :return: dictionary with client's raw portfolio and some statistics.
1667        """
1668        if self.accountId is None or not self.accountId:
1669            uLogger.error("Variable `accountId` must be defined for using this method!")
1670            raise Exception("Account ID required")
1671
1672        view = {
1673            "raw": {  # --- raw portfolio responses from broker with user portfolio data:
1674                "headers": {},  # list of dictionaries, response headers without "positions" section
1675                "Currencies": [],  # list of dictionaries, open trades with currencies from "positions" section
1676                "Shares": [],  # list of dictionaries, open trades with shares from "positions" section
1677                "Bonds": [],  # list of dictionaries, open trades with bonds from "positions" section
1678                "Etfs": [],  # list of dictionaries, open trades with etfs from "positions" section
1679                "Futures": [],  # list of dictionaries, open trades with futures from "positions" section
1680                "positions": {},  # raw response from broker: dictionary with current available or blocked currencies and instruments for client
1681                "orders": [],  # raw response from broker: list of dictionaries with all pending (market) orders
1682                "stopOrders": [],  # raw response from broker: list of dictionaries with all stop orders
1683                "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}},  # dict with prices of all currencies in RUB
1684            },
1685            "stat": {  # --- some statistics calculated using "raw" sections:
1686                "portfolioCostRUB": 0.,  # portfolio cost in RUB (Russian Rouble)
1687                "availableRUB": 0.,  # available rubles (without other currencies)
1688                "blockedRUB": 0.,  # blocked sum in Russian Rouble
1689                "totalChangesRUB": 0.,  # changes for all open trades in RUB
1690                "totalChangesPercentRUB": 0.,  # changes for all open trades in percents
1691                "allCurrenciesCostRUB": 0.,  # costs of all currencies (include rubles) in RUB
1692                "sharesCostRUB": 0.,  # costs of all shares in RUB
1693                "bondsCostRUB": 0.,  # costs of all bonds in RUB
1694                "etfsCostRUB": 0.,  # costs of all etfs in RUB
1695                "futuresCostRUB": 0.,  # costs of all futures in RUB
1696                "Currencies": [],  # list of dictionaries of all currencies statistics
1697                "Shares": [],  # list of dictionaries of all shares statistics
1698                "Bonds": [],  # list of dictionaries of all bonds statistics
1699                "Etfs": [],  # list of dictionaries of all etfs statistics
1700                "Futures": [],  # list of dictionaries of all futures statistics
1701                "orders": [],  # list of dictionaries of all pending (market) orders and it's parameters
1702                "stopOrders": [],  # list of dictionaries of all stop orders and it's parameters
1703                "blockedCurrencies": {},  # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21}
1704                "blockedInstruments": {},  # dict with blocked  by FIGI, e.g. {}
1705                "funds": {},  # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1706            },
1707            "analytics": {  # --- some analytics of portfolio:
1708                "distrByAssets": {},  # portfolio distribution by assets
1709                "distrByCompanies": {},  # portfolio distribution by companies
1710                "distrBySectors": {},  # portfolio distribution by sectors
1711                "distrByCurrencies": {},  # portfolio distribution by currencies
1712                "distrByCountries": {},  # portfolio distribution by countries
1713                "bondsCalendar": None,  # bonds payment calendar as Pandas DataFrame (if these present in portfolio)
1714            }
1715        }
1716
1717        details = details.lower()
1718        availableDetails = ["full", "positions", "orders", "analytics", "calendar", "digest"]
1719        if details not in availableDetails:
1720            details = "full"
1721            uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails))
1722
1723        uLogger.debug("Requesting portfolio of a client. Wait, please...")
1724
1725        portfolioResponse = self.RequestPortfolio()  # current user's portfolio (dict)
1726        view["raw"]["positions"] = self.RequestPositions()  # current open positions by instruments (dict)
1727        view["raw"]["orders"] = self.RequestPendingOrders()  # current actual pending limit orders (list)
1728        view["raw"]["stopOrders"] = self.RequestStopOrders()  # current actual stop orders (list)
1729
1730        # save response headers without "positions" section:
1731        for key in portfolioResponse.keys():
1732            if key != "positions":
1733                view["raw"]["headers"][key] = portfolioResponse[key]
1734
1735            else:
1736                continue
1737
1738        # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation
1739        # Type of instrument must be only one of supported types in TKS_INSTRUMENTS
1740        for item in portfolioResponse["positions"]:
1741            if item["instrumentType"] == "currency":
1742                self._figi = item["figi"]
1743                if not self._figi and item["ticker"]:
1744                    self._ticker = item["ticker"]
1745                    self._figi = self.SearchByTicker()["figi"]  # Get FIGI to avoid warnings
1746
1747                curr = self.SearchByFIGI(requestPrice=False)
1748
1749                # current price of currency in RUB:
1750                view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = {
1751                    "name": curr["name"],
1752                    "currentPrice": NanoToFloat(
1753                        item["currentPrice"]["units"],
1754                        item["currentPrice"]["nano"]
1755                    ),
1756                }
1757
1758                view["raw"]["Currencies"].append(item)
1759
1760            elif item["instrumentType"] == "share":
1761                view["raw"]["Shares"].append(item)
1762
1763            elif item["instrumentType"] == "bond":
1764                view["raw"]["Bonds"].append(item)
1765
1766            elif item["instrumentType"] == "etf":
1767                view["raw"]["Etfs"].append(item)
1768
1769            elif item["instrumentType"] == "futures":
1770                view["raw"]["Futures"].append(item)
1771
1772            else:
1773                continue
1774
1775        # how many volume of currencies (by ISO currency name) are blocked:
1776        for item in view["raw"]["positions"]["blocked"]:
1777            blocked = NanoToFloat(item["units"], item["nano"])
1778            if blocked > 0:
1779                view["stat"]["blockedCurrencies"][item["currency"]] = blocked
1780
1781        # how many volume of instruments (by FIGI) are blocked:
1782        for item in view["raw"]["positions"]["securities"]:
1783            blocked = int(item["blocked"])
1784            if blocked > 0:
1785                view["stat"]["blockedInstruments"][item["figi"]] = blocked
1786
1787        allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]}
1788
1789        if "rub" in allBlocked.keys():
1790            view["stat"]["blockedRUB"] = allBlocked["rub"]  # blocked rubles
1791
1792        # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies:
1793        view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"])
1794        view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"])
1795        view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"])
1796        view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"])
1797        view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"])
1798        view["stat"]["portfolioCostRUB"] = sum([
1799            view["stat"]["allCurrenciesCostRUB"],
1800            view["stat"]["sharesCostRUB"],
1801            view["stat"]["bondsCostRUB"],
1802            view["stat"]["etfsCostRUB"],
1803            view["stat"]["futuresCostRUB"],
1804        ])
1805
1806        # --- calculating some portfolio statistics:
1807        byComp = {}  # distribution by companies
1808        bySect = {}  # distribution by sectors
1809        byCurr = {}  # distribution by currencies (include RUB)
1810        unknownCountryName = "All other countries"  # default name for instruments without "countryOfRisk" and "countryOfRiskName"
1811        byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}}  # distribution by countries (currencies are included in their countries)
1812
1813        for item in portfolioResponse["positions"]:
1814            self._figi = item["figi"]
1815            if not self._figi and item["ticker"]:
1816                self._ticker = item["ticker"]
1817                self._figi = self.SearchByTicker()["figi"]  # Get FIGI to avoid warnings
1818
1819            instrument = self.SearchByFIGI(requestPrice=False)  # full raw info about instrument by FIGI
1820
1821            if instrument:
1822                if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys():
1823                    blocked = allBlocked[instrument["nominal"]["currency"]]  # blocked volume of currency
1824
1825                elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys():
1826                    blocked = allBlocked[item["figi"]]  # blocked volume of other instruments
1827
1828                else:
1829                    blocked = 0
1830
1831                volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"])  # available volume of instrument
1832                lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"])  # available volume in lots of instrument
1833                direction = "Long" if lots >= 0 else "Short"  # direction of an instrument's position: short or long
1834                curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"])  # current instrument's price
1835                average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"])  # current average position price
1836                profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"])  # expected profit at current moment
1837                currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"]  # currency name rub, usd, eur etc.
1838                cost = curPrice if "currentNkd" not in item.keys() else (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume  # current cost of all volume of instrument in basic asset
1839                baseCurrencyName = item["currentPrice"]["currency"]  # name of base currency (rub)
1840                countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName
1841                costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"]  # cost in rubles
1842                percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.  # instrument's part in percent of full portfolio cost
1843
1844                statData = {
1845                    "figi": item["figi"],  # FIGI from REST API "GetPortfolio" method
1846                    "ticker": instrument["ticker"],  # ticker by FIGI
1847                    "currency": currency,  # currency name rub, usd, eur etc. for instrument price
1848                    "volume": volume,  # available volume of instrument
1849                    "lots": lots,  # volume in lots of instrument
1850                    "direction": direction,  # direction of an instrument's position: short or long
1851                    "blocked": blocked,  # blocked volume of currency or instrument
1852                    "currentPrice": curPrice,  # current instrument's price in basic asset
1853                    "average": average,  # current average position price
1854                    "cost": cost,  # current cost of all volume of instrument in basic asset
1855                    "baseCurrencyName": baseCurrencyName,  # name of base currency (rub)
1856                    "costRUB": costRUB,  # cost of instrument in ruble
1857                    "percentCostRUB": percentCostRUB,  # instrument's part in percent of full portfolio cost in RUB
1858                    "profit": profit,  # expected profit at current moment
1859                    "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0,  # expected percents of profit at current moment for this instrument
1860                    "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other",
1861                    "name": instrument["name"] if "name" in instrument.keys() else "",  # human-readable names of instruments
1862                    "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "",  # ISO name for currencies only
1863                    "country": countryName,  # e.g. "[RU] Российская Федерация" or unknownCountryName
1864                    "step": instrument["step"],  # minimum price increment
1865                }
1866
1867                # adding distribution by unique countries:
1868                if statData["country"] not in byCountry.keys():
1869                    byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB}
1870
1871                else:
1872                    byCountry[statData["country"]]["cost"] += costRUB
1873                    byCountry[statData["country"]]["percent"] += percentCostRUB
1874
1875                if item["instrumentType"] != "currency":
1876                    # adding distribution by unique companies:
1877                    if statData["name"]:
1878                        if statData["name"] not in byComp.keys():
1879                            byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB}
1880
1881                        else:
1882                            byComp[statData["name"]]["cost"] += costRUB
1883                            byComp[statData["name"]]["percent"] += percentCostRUB
1884
1885                    # adding distribution by unique sectors:
1886                    if statData["sector"] not in bySect.keys():
1887                        bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB}
1888
1889                    else:
1890                        bySect[statData["sector"]]["cost"] += costRUB
1891                        bySect[statData["sector"]]["percent"] += percentCostRUB
1892
1893                # adding distribution by unique currencies:
1894                if currency not in byCurr.keys():
1895                    byCurr[currency] = {
1896                        "name": view["raw"]["currenciesCurrentPrices"][currency]["name"],
1897                        "cost": costRUB,
1898                        "percent": percentCostRUB
1899                    }
1900
1901                else:
1902                    byCurr[currency]["cost"] += costRUB
1903                    byCurr[currency]["percent"] += percentCostRUB
1904
1905                # saving statistics for every instrument:
1906                if item["instrumentType"] == "currency":
1907                    view["stat"]["Currencies"].append(statData)
1908
1909                    # update dict with free funds for trading (total - blocked) by currencies
1910                    # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1911                    view["stat"]["funds"][currency] = {
1912                        "total": volume,
1913                        "totalCostRUB": costRUB,  # total volume cost in rubles
1914                        "free": volume - blocked,
1915                        "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0,  # free volume cost in rubles
1916                    }
1917
1918                elif item["instrumentType"] == "share":
1919                    view["stat"]["Shares"].append(statData)
1920
1921                elif item["instrumentType"] == "bond":
1922                    view["stat"]["Bonds"].append(statData)
1923
1924                elif item["instrumentType"] == "etf":
1925                    view["stat"]["Etfs"].append(statData)
1926
1927                elif item["instrumentType"] == "Futures":
1928                    view["stat"]["Futures"].append(statData)
1929
1930                else:
1931                    continue
1932
1933        # total changes in Russian Ruble:
1934        view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]])  # available RUB without other currencies
1935        view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0.
1936        startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100)
1937        view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost
1938        view["stat"]["funds"]["rub"] = {
1939            "total": view["stat"]["availableRUB"],
1940            "totalCostRUB": view["stat"]["availableRUB"],
1941            "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1942            "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1943        }
1944
1945        # --- pending limit orders sector data:
1946        uniquePendingOrdersFIGIs = []  # unique FIGIs of pending limit orders to avoid many times price requests
1947        uniquePendingOrders = {}  # unique instruments with FIGIs as dictionary keys
1948
1949        for item in view["raw"]["orders"]:
1950            self._figi = item["figi"]
1951
1952            if item["figi"] not in uniquePendingOrdersFIGIs:
1953                instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI, price requests only one time
1954
1955                uniquePendingOrdersFIGIs.append(item["figi"])
1956                uniquePendingOrders[item["figi"]] = instrument
1957
1958            else:
1959                instrument = uniquePendingOrders[item["figi"]]
1960
1961            if instrument:
1962                action = TKS_ORDER_DIRECTIONS[item["direction"]]
1963                orderType = TKS_ORDER_TYPES[item["orderType"]]
1964                orderState = TKS_ORDER_STATES[item["executionReportStatus"]]
1965                orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1966
1967                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1968                if item["direction"] == "ORDER_DIRECTION_BUY":
1969                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
1970
1971                else:
1972                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
1973
1974                # requested price for order execution:
1975                target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"])
1976
1977                # necessary changes in percent to reach target from current price:
1978                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
1979
1980                view["stat"]["orders"].append({
1981                    "orderID": item["orderId"],  # orderId number parameter of current order
1982                    "figi": item["figi"],  # FIGI identification
1983                    "ticker": instrument["ticker"],  # ticker name by FIGI
1984                    "lotsRequested": item["lotsRequested"],  # requested lots value
1985                    "lotsExecuted": item["lotsExecuted"],  # how many lots are executed
1986                    "currentPrice": lastPrice,  # current instrument's price for defined action
1987                    "targetPrice": target,  # requested price for order execution in base currency
1988                    "baseCurrencyName": item["initialSecurityPrice"]["currency"],  # name of base currency
1989                    "percentChanges": changes,  # changes in percent to target from current price
1990                    "currency": item["currency"],  # instrument's currency name
1991                    "action": action,  # sell / buy / Unknown from TKS_ORDER_DIRECTIONS
1992                    "type": orderType,  # type of order from TKS_ORDER_TYPES
1993                    "status": orderState,  # order status from TKS_ORDER_STATES
1994                    "date": orderDate,  # string with order date and time from UTC format (without nano seconds part)
1995                })
1996
1997        # --- stop orders sector data:
1998        uniqueStopOrdersFIGIs = []  # unique FIGIs of stop orders to avoid many times price requests
1999        uniqueStopOrders = {}  # unique instruments with FIGIs as dictionary keys
2000
2001        for item in view["raw"]["stopOrders"]:
2002            self._figi = item["figi"]
2003
2004            if item["figi"] not in uniqueStopOrdersFIGIs:
2005                instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI, price requests only one time
2006
2007                uniqueStopOrdersFIGIs.append(item["figi"])
2008                uniqueStopOrders[item["figi"]] = instrument
2009
2010            else:
2011                instrument = uniqueStopOrders[item["figi"]]
2012
2013            if instrument:
2014                action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]]
2015                orderType = TKS_STOP_ORDER_TYPES[item["orderType"]]
2016                createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
2017
2018                # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order
2019                if "expirationTime" in item.keys():
2020                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"]
2021                    expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0]
2022
2023                else:
2024                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"]
2025                    expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"]
2026
2027                # current instrument's price (last sellers order if buy, and last buyers order if sell):
2028                if item["direction"] == "STOP_ORDER_DIRECTION_BUY":
2029                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
2030
2031                else:
2032                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
2033
2034                # requested price when stop-order executed:
2035                target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"])
2036
2037                # price for limit-order, set up when stop-order executed:
2038                limit = NanoToFloat(item["price"]["units"], item["price"]["nano"])
2039
2040                # necessary changes in percent to reach target from current price:
2041                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
2042
2043                view["stat"]["stopOrders"].append({
2044                    "orderID": item["stopOrderId"],  # stopOrderId number parameter of current stop-order
2045                    "figi": item["figi"],  # FIGI identification
2046                    "ticker": instrument["ticker"],  # ticker name by FIGI
2047                    "lotsRequested": item["lotsRequested"],  # requested lots value
2048                    "currentPrice": lastPrice,  # current instrument's price for defined action
2049                    "targetPrice": target,  # requested price for stop-order execution in base currency
2050                    "limitPrice": limit,  # price for limit-order, set up when stop-order executed, 0 if market order
2051                    "baseCurrencyName": item["stopPrice"]["currency"],  # name of base currency
2052                    "percentChanges": changes,  # changes in percent to target from current price
2053                    "currency": item["currency"],  # instrument's currency name
2054                    "action": action,  # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS
2055                    "type": orderType,  # type of order from TKS_STOP_ORDER_TYPES
2056                    "expType": expType,  # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES
2057                    "createDate": createDate,  # string with created order date and time from UTC format (without nano seconds part)
2058                    "expDate": expDate,  # string with expiration order date and time from UTC format (without nano seconds part)
2059                })
2060
2061        # --- calculating data for analytics section:
2062        # portfolio distribution by assets:
2063        view["analytics"]["distrByAssets"] = {
2064            "Ruble": {
2065                "uniques": 1,
2066                "cost": view["stat"]["availableRUB"],
2067                "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2068            },
2069            "Currencies": {
2070                "uniques": len(view["stat"]["Currencies"]),  # all foreign currencies without RUB
2071                "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"],
2072                "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2073            },
2074            "Shares": {
2075                "uniques": len(view["stat"]["Shares"]),
2076                "cost": view["stat"]["sharesCostRUB"],
2077                "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2078            },
2079            "Bonds": {
2080                "uniques": len(view["stat"]["Bonds"]),
2081                "cost": view["stat"]["bondsCostRUB"],
2082                "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2083            },
2084            "Etfs": {
2085                "uniques": len(view["stat"]["Etfs"]),
2086                "cost": view["stat"]["etfsCostRUB"],
2087                "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2088            },
2089            "Futures": {
2090                "uniques": len(view["stat"]["Futures"]),
2091                "cost": view["stat"]["futuresCostRUB"],
2092                "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2093            },
2094        }
2095
2096        # portfolio distribution by companies:
2097        view["analytics"]["distrByCompanies"]["All money cash"] = {
2098            "ticker": "",
2099            "cost": view["stat"]["allCurrenciesCostRUB"],
2100            "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2101        }
2102        view["analytics"]["distrByCompanies"].update(byComp)
2103
2104        # portfolio distribution by sectors:
2105        view["analytics"]["distrBySectors"]["All money cash"] = {
2106            "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"],
2107            "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"],
2108        }
2109        view["analytics"]["distrBySectors"].update(bySect)
2110
2111        # portfolio distribution by currencies:
2112        if "rub" not in view["analytics"]["distrByCurrencies"].keys():
2113            view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0}
2114
2115            if self.moreDebug:
2116                uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!")
2117
2118        view["analytics"]["distrByCurrencies"].update(byCurr)
2119        view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
2120        view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
2121
2122        # portfolio distribution by countries:
2123        if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys():
2124            view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0}
2125
2126            if self.moreDebug:
2127                uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!")
2128
2129        view["analytics"]["distrByCountries"].update(byCountry)
2130        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
2131        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
2132
2133        # --- Prepare text statistics overview in human-readable:
2134        if show or onlyFiles:
2135            actualOnDate = datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)
2136
2137            # Whatever the value `details`, header not changes:
2138            info = [
2139                "# Client's portfolio\n\n",
2140                "* **Actual on date:** [{} UTC]\n".format(actualOnDate),
2141                "* **Account ID:** [{}]\n".format(self.accountId),
2142            ]
2143
2144            if details in ["full", "positions", "digest"]:
2145                info.extend([
2146                    "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2147                    "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format(
2148                        "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2149                        view["stat"]["totalChangesRUB"],
2150                        "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2151                        view["stat"]["totalChangesPercentRUB"],
2152                    ),
2153                ])
2154
2155            if details in ["full", "positions"]:
2156                info.extend([
2157                    "## Open positions\n\n",
2158                    "| Ticker [FIGI]               | Volume (blocked)                | Lots     | Curr. price  | Avg. price   | Current volume cost | Profit (%)                   |\n",
2159                    "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n",
2160                    "| **Ruble:**                  | {:>31} |          |              |              |                     |                              |\n".format(
2161                        "{:.2f} ({:.2f}) rub".format(
2162                            view["stat"]["availableRUB"],
2163                            view["stat"]["blockedRUB"],
2164                        )
2165                    )
2166                ])
2167
2168                def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list:
2169                    return [
2170                        "|                             |                                 |          |              |              |                     |                              |\n",
2171                        "| {:<27} |                                 |          |              |              | {:>19} |                              |\n".format(
2172                            noTradeStr if noTradeStr else typeStr,
2173                            "" if noTradeStr else "{:.2f} RUB".format(CostRUB),
2174                        ),
2175                    ]
2176
2177                def _InfoStr(data: dict, isCurr: bool = False) -> str:
2178                    return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format(
2179                        "{} [{}]".format(data["ticker"], data["figi"]),
2180                        "{:.2f} ({:.2f}) {}".format(
2181                            data["volume"],
2182                            data["blocked"],
2183                            data["currency"],
2184                        ) if isCurr else "{:.0f} ({:.0f})".format(
2185                            data["volume"],
2186                            data["blocked"],
2187                        ),
2188                        "—" if isCurr else "{:.4f}".format(data["lots"]).rstrip("0").rstrip("."),
2189                        "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a",
2190                        "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a",
2191                        "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]),
2192                        "{}{:.2f} {} ({}{:.2f}%)".format(
2193                            "+" if data["profit"] > 0 else "",
2194                            data["profit"], data["baseCurrencyName"],
2195                            "+" if data["percentProfit"] > 0 else "",
2196                            data["percentProfit"],
2197                        ),
2198                    )
2199
2200                # --- Show currencies section:
2201                if view["stat"]["Currencies"]:
2202                    info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**"))
2203                    for item in view["stat"]["Currencies"]:
2204                        info.append(_InfoStr(item, isCurr=True))
2205
2206                else:
2207                    info.extend(_SplitStr(noTradeStr="**Currencies:** no trades"))
2208
2209                # --- Show shares section:
2210                if view["stat"]["Shares"]:
2211                    info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**"))
2212
2213                    for item in view["stat"]["Shares"]:
2214                        info.append(_InfoStr(item))
2215
2216                else:
2217                    info.extend(_SplitStr(noTradeStr="**Shares:** no trades"))
2218
2219                # --- Show bonds section:
2220                if view["stat"]["Bonds"]:
2221                    info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**"))
2222
2223                    for item in view["stat"]["Bonds"]:
2224                        info.append(_InfoStr(item))
2225
2226                else:
2227                    info.extend(_SplitStr(noTradeStr="**Bonds:** no trades"))
2228
2229                # --- Show etfs section:
2230                if view["stat"]["Etfs"]:
2231                    info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**"))
2232
2233                    for item in view["stat"]["Etfs"]:
2234                        info.append(_InfoStr(item))
2235
2236                else:
2237                    info.extend(_SplitStr(noTradeStr="**Etfs:** no trades"))
2238
2239                # --- Show futures section:
2240                if view["stat"]["Futures"]:
2241                    info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**"))
2242
2243                    for item in view["stat"]["Futures"]:
2244                        info.append(_InfoStr(item))
2245
2246                else:
2247                    info.extend(_SplitStr(noTradeStr="**Futures:** no trades"))
2248
2249            if details in ["full", "orders"]:
2250                # --- Show pending limit orders section:
2251                if view["stat"]["orders"]:
2252                    info.extend([
2253                        "\n## Opened pending limit-orders: [{}]\n".format(len(view["stat"]["orders"])),
2254                        "\n| Ticker [FIGI]               | Order ID       | Lots (exec.) | Current price (% delta) | Target price  | Action    | Type      | Create date (UTC)       |\n",
2255                        "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n",
2256                    ])
2257
2258                    for item in view["stat"]["orders"]:
2259                        info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format(
2260                            "{} [{}]".format(item["ticker"], item["figi"]),
2261                            item["orderID"],
2262                            "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]),
2263                            "{} {} ({}{:.2f}%)".format(
2264                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2265                                item["baseCurrencyName"],
2266                                "+" if item["percentChanges"] > 0 else "",
2267                                float(item["percentChanges"]),
2268                            ),
2269                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2270                            item["action"],
2271                            item["type"],
2272                            item["date"],
2273                        ))
2274
2275                else:
2276                    info.append("\n## Total pending limit-orders: [0]\n")
2277
2278                # --- Show stop orders section:
2279                if view["stat"]["stopOrders"]:
2280                    info.extend([
2281                        "\n## Opened stop-orders: [{}]\n".format(len(view["stat"]["stopOrders"])),
2282                        "\n| Ticker [FIGI]               | Stop order ID                        | Lots   | Current price (% delta) | Target price  | Limit price   | Action    | Type        | Expire type  | Create date (UTC)   | Expiration (UTC)    |\n",
2283                        "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n",
2284                    ])
2285
2286                    for item in view["stat"]["stopOrders"]:
2287                        info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format(
2288                            "{} [{}]".format(item["ticker"], item["figi"]),
2289                            item["orderID"],
2290                            item["lotsRequested"],
2291                            "{} {} ({}{:.2f}%)".format(
2292                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2293                                item["baseCurrencyName"],
2294                                "+" if item["percentChanges"] > 0 else "",
2295                                float(item["percentChanges"]),
2296                            ),
2297                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2298                            "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"],
2299                            item["action"],
2300                            item["type"],
2301                            item["expType"],
2302                            item["createDate"],
2303                            item["expDate"],
2304                        ))
2305
2306                else:
2307                    info.append("\n## Total stop-orders: [0]\n")
2308
2309            if details in ["full", "analytics"]:
2310                # -- Show analytics section:
2311                if view["stat"]["portfolioCostRUB"] > 0:
2312                    info.extend([
2313                        "\n# Analytics\n\n"
2314                        "* **Actual on date:** [{} UTC]\n".format(actualOnDate),
2315                        "* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2316                        "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format(
2317                            "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2318                            view["stat"]["totalChangesRUB"],
2319                            "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2320                            view["stat"]["totalChangesPercentRUB"],
2321                        ),
2322                        "\n## Portfolio distribution by assets\n"
2323                        "\n| Type                               | Uniques | Percent | Current cost       |\n",
2324                        "|------------------------------------|---------|---------|--------------------|\n",
2325                    ])
2326
2327                    for key in view["analytics"]["distrByAssets"].keys():
2328                        if view["analytics"]["distrByAssets"][key]["cost"] > 0:
2329                            info.append("| {:<34} | {:<7} | {:<7} | {:<18} |\n".format(
2330                                key,
2331                                view["analytics"]["distrByAssets"][key]["uniques"],
2332                                "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]),
2333                                "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]),
2334                            ))
2335
2336                    aSepLine = "|----------------------------------------------|---------|--------------------|\n"
2337
2338                    info.extend([
2339                        "\n## Portfolio distribution by companies\n"
2340                        "\n| Company                                      | Percent | Current cost       |\n",
2341                        aSepLine,
2342                    ])
2343
2344                    for company in view["analytics"]["distrByCompanies"].keys():
2345                        if view["analytics"]["distrByCompanies"][company]["cost"] > 0:
2346                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2347                                "{}{}".format(
2348                                    "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "",
2349                                    company,
2350                                ),
2351                                "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]),
2352                                "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]),
2353                            ))
2354
2355                    info.extend([
2356                        "\n## Portfolio distribution by sectors\n"
2357                        "\n| Sector                                       | Percent | Current cost       |\n",
2358                        aSepLine,
2359                    ])
2360
2361                    for sector in view["analytics"]["distrBySectors"].keys():
2362                        if view["analytics"]["distrBySectors"][sector]["cost"] > 0:
2363                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2364                                sector,
2365                                "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]),
2366                                "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]),
2367                            ))
2368
2369                    info.extend([
2370                        "\n## Portfolio distribution by currencies\n"
2371                        "\n| Instruments currencies                       | Percent | Current cost       |\n",
2372                        aSepLine,
2373                    ])
2374
2375                    for curr in view["analytics"]["distrByCurrencies"].keys():
2376                        if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0:
2377                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2378                                "[{}] {}".format(curr, view["analytics"]["distrByCurrencies"][curr]["name"]),
2379                                "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]),
2380                                "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]),
2381                            ))
2382
2383                    info.extend([
2384                        "\n## Portfolio distribution by countries\n"
2385                        "\n| Assets by country                            | Percent | Current cost       |\n",
2386                        aSepLine,
2387                    ])
2388
2389                    for country in view["analytics"]["distrByCountries"].keys():
2390                        if view["analytics"]["distrByCountries"][country]["cost"] > 0:
2391                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2392                                country,
2393                                "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]),
2394                                "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]),
2395                            ))
2396
2397            if details in ["full", "calendar"]:
2398                # -- Show bonds payment calendar section:
2399                if view["stat"]["Bonds"]:
2400                    bondTickers = [item["ticker"] for item in view["stat"]["Bonds"]]
2401                    view["analytics"]["bondsCalendar"] = self.ExtendBondsData(instruments=bondTickers, xlsx=False)
2402                    info.append("\n" + self.ShowBondsCalendar(extBonds=view["analytics"]["bondsCalendar"], show=False))
2403
2404                else:
2405                    info.append("\n# Bond payments calendar\n\nNo bonds in the portfolio to create payments calendar\n")
2406
2407            infoText = "".join(info)
2408
2409            if show and not onlyFiles:
2410                uLogger.info(infoText)
2411
2412            if details == "full" and self.overviewFile:
2413                filename = self.overviewFile
2414
2415            elif details == "digest" and self.overviewDigestFile:
2416                filename = self.overviewDigestFile
2417
2418            elif details == "positions" and self.overviewPositionsFile:
2419                filename = self.overviewPositionsFile
2420
2421            elif details == "orders" and self.overviewOrdersFile:
2422                filename = self.overviewOrdersFile
2423
2424            elif details == "analytics" and self.overviewAnalyticsFile:
2425                filename = self.overviewAnalyticsFile
2426
2427            elif details == "calendar" and self.overviewBondsCalendarFile:
2428                filename = self.overviewBondsCalendarFile
2429
2430            else:
2431                filename = ""
2432
2433            if filename and (show or onlyFiles):
2434                with open(filename, "w", encoding="UTF-8") as fH:
2435                    fH.write(infoText)
2436
2437                uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename)))
2438
2439                if self.useHTMLReports:
2440                    htmlFilePath = filename.replace(".md", ".html") if filename.endswith(".md") else filename + ".html"
2441                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
2442                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Client's portfolio", commonCSS=COMMON_CSS, markdown=infoText))
2443
2444                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
2445
2446        return view
2447
2448    def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True, onlyFiles=False) -> tuple[list[dict], dict]:
2449        """
2450        Returns history operations between two given dates for current `accountId`.
2451        If `reportFile` string is not empty then also save human-readable report.
2452        Shows some statistical data of closed positions.
2453
2454        :param start: see docstring in `TradeRoutines.GetDatesAsString()` method.
2455        :param end: see docstring in `TradeRoutines.GetDatesAsString()` method.
2456        :param show: if `True` then also prints all records to the console.
2457        :param showCancelled: if `False` then remove information about cancelled operations from the deals report.
2458        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
2459        :return: original list of dictionaries with history of deals records from API ("operations" key):
2460                 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2461                 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc.
2462        """
2463        if self.accountId is None or not self.accountId:
2464            uLogger.error("Variable `accountId` must be defined for using this method!")
2465            raise Exception("Account ID required")
2466
2467        startDate, endDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT)  # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2468
2469        uLogger.debug("Requesting history of a client's operations. Wait, please...")
2470
2471        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2472        dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations"
2473        self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate})
2474        ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"]  # list of dict: operations returns by broker
2475        customStat = {}  # custom statistics in additional to responseJSON
2476
2477        # --- output report in human-readable format:
2478        if self.reportFile and (show or onlyFiles):
2479            splitLine1 = "|                            |                               |                              |                      |                        |\n"  # Summary section
2480            splitLine2 = "|                     |              |              |            |           |                 |            |                                                                    |\n"  # Operations section
2481            nextDay = ""
2482
2483            info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])]
2484
2485            if len(ops) > 0:
2486                customStat = {
2487                    "opsCount": 0,  # total operations count
2488                    "buyCount": 0,  # buy operations
2489                    "sellCount": 0,  # sell operations
2490                    "buyTotal": {"rub": 0.},  # Buy sums in different currencies
2491                    "sellTotal": {"rub": 0.},  # Sell sums in different currencies
2492                    "payIn": {"rub": 0.},  # Deposit brokerage account
2493                    "payOut": {"rub": 0.},  # Withdrawals
2494                    "divs": {"rub": 0.},  # Dividends income
2495                    "coupons": {"rub": 0.},  # Coupon's income
2496                    "brokerCom": {"rub": 0.},  # Service commissions
2497                    "serviceCom": {"rub": 0.},  # Service commissions
2498                    "marginCom": {"rub": 0.},  # Margin commissions
2499                    "allTaxes": {"rub": 0.},  # Sum of withholding taxes and corrections
2500                }
2501
2502                # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES:
2503                for item in ops:
2504                    if item["state"] == "OPERATION_STATE_EXECUTED":
2505                        payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2506
2507                        # count buy operations:
2508                        if "_BUY" in item["operationType"]:
2509                            customStat["buyCount"] += 1
2510
2511                            if item["payment"]["currency"] in customStat["buyTotal"].keys():
2512                                customStat["buyTotal"][item["payment"]["currency"]] += payment
2513
2514                            else:
2515                                customStat["buyTotal"][item["payment"]["currency"]] = payment
2516
2517                        # count sell operations:
2518                        elif "_SELL" in item["operationType"]:
2519                            customStat["sellCount"] += 1
2520
2521                            if item["payment"]["currency"] in customStat["sellTotal"].keys():
2522                                customStat["sellTotal"][item["payment"]["currency"]] += payment
2523
2524                            else:
2525                                customStat["sellTotal"][item["payment"]["currency"]] = payment
2526
2527                        # count incoming operations:
2528                        elif item["operationType"] in ["OPERATION_TYPE_INPUT"]:
2529                            if item["payment"]["currency"] in customStat["payIn"].keys():
2530                                customStat["payIn"][item["payment"]["currency"]] += payment
2531
2532                            else:
2533                                customStat["payIn"][item["payment"]["currency"]] = payment
2534
2535                        # count withdrawals operations:
2536                        elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]:
2537                            if item["payment"]["currency"] in customStat["payOut"].keys():
2538                                customStat["payOut"][item["payment"]["currency"]] += payment
2539
2540                            else:
2541                                customStat["payOut"][item["payment"]["currency"]] = payment
2542
2543                        # count dividends income:
2544                        elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]:
2545                            if item["payment"]["currency"] in customStat["divs"].keys():
2546                                customStat["divs"][item["payment"]["currency"]] += payment
2547
2548                            else:
2549                                customStat["divs"][item["payment"]["currency"]] = payment
2550
2551                        # count coupon's income:
2552                        elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]:
2553                            if item["payment"]["currency"] in customStat["coupons"].keys():
2554                                customStat["coupons"][item["payment"]["currency"]] += payment
2555
2556                            else:
2557                                customStat["coupons"][item["payment"]["currency"]] = payment
2558
2559                        # count broker commissions:
2560                        elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]:
2561                            if item["payment"]["currency"] in customStat["brokerCom"].keys():
2562                                customStat["brokerCom"][item["payment"]["currency"]] += payment
2563
2564                            else:
2565                                customStat["brokerCom"][item["payment"]["currency"]] = payment
2566
2567                        # count service commissions:
2568                        elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]:
2569                            if item["payment"]["currency"] in customStat["serviceCom"].keys():
2570                                customStat["serviceCom"][item["payment"]["currency"]] += payment
2571
2572                            else:
2573                                customStat["serviceCom"][item["payment"]["currency"]] = payment
2574
2575                        # count margin commissions:
2576                        elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]:
2577                            if item["payment"]["currency"] in customStat["marginCom"].keys():
2578                                customStat["marginCom"][item["payment"]["currency"]] += payment
2579
2580                            else:
2581                                customStat["marginCom"][item["payment"]["currency"]] = payment
2582
2583                        # count withholding taxes:
2584                        elif "_TAX" in item["operationType"]:
2585                            if item["payment"]["currency"] in customStat["allTaxes"].keys():
2586                                customStat["allTaxes"][item["payment"]["currency"]] += payment
2587
2588                            else:
2589                                customStat["allTaxes"][item["payment"]["currency"]] = payment
2590
2591                        else:
2592                            continue
2593
2594                customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"]
2595
2596                # --- view "Actions" lines:
2597                info.extend([
2598                    "| Report sections            |                               |                              |                      |                        |\n",
2599                    "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n",
2600                    "| **Actions:**               | Trades: {:<21} | Trading volumes:             |                      |                        |\n".format(customStat["opsCount"]),
2601                    "|                            |   Buy: {:<22} | {:<28} |                      |                        |\n".format(
2602                        "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2603                        "  rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else "  —",
2604                    ),
2605                    "|                            |   Sell: {:<21} | {:<28} |                      |                        |\n".format(
2606                        "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2607                        "  rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else "  —",
2608                    ),
2609                ])
2610
2611                opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys()))))
2612                for key in opsKeys:
2613                    if key == "rub":
2614                        continue
2615
2616                    info.extend([
2617                        "|                            |                               | {:<28} |                      |                        |\n".format(
2618                            "  {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0)
2619                        ),
2620                        "|                            |                               | {:<28} |                      |                        |\n".format(
2621                            "  {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0)
2622                        ),
2623                    ])
2624
2625                info.append(splitLine1)
2626
2627                def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str:
2628                    return "|                            | {:<29} | {:<28} | {:<20} | {:<22} |\n".format(
2629                            "  {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else "  —",
2630                            "  {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else "  —",
2631                            "  {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else "  —",
2632                            "  {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else "  —",
2633                    )
2634
2635                # --- view "Payments" lines:
2636                info.append("| **Payments:**              | Deposit on broker account:    | Withdrawals:                 | Dividends income:    | Coupons income:        |\n")
2637                paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys()))))
2638
2639                for key in paymentsKeys:
2640                    info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key))
2641
2642                info.append(splitLine1)
2643
2644                # --- view "Commissions and taxes" lines:
2645                info.append("| **Commissions and taxes:** | Broker commissions:           | Service commissions:         | Margin commissions:  | All taxes/corrections: |\n")
2646                comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys()))))
2647
2648                for key in comKeys:
2649                    info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key))
2650
2651                info.extend([
2652                    "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"),
2653                    "| Date and time       | FIGI         | Ticker       | Asset      | Value     | Payment         | Status     | Operation type                                                     |\n",
2654                    "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n",
2655                ])
2656
2657            else:
2658                info.append("Broker returned no operations during this period\n")
2659
2660            # --- view "Operations" section:
2661            for item in ops:
2662                if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]:
2663                    continue
2664
2665                else:
2666                    self._figi = item["figi"]
2667                    payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2668                    instrument = self.SearchByFIGI(requestPrice=False) if self._figi else {}
2669
2670                    # group of deals during one day:
2671                    if nextDay and item["date"].split("T")[0] != nextDay:
2672                        info.append(splitLine2)
2673                        nextDay = ""
2674
2675                    else:
2676                        nextDay = item["date"].split("T")[0]  # saving current day for splitting
2677
2678                    info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format(
2679                        item["date"].replace("T", " ").replace("Z", "").split(".")[0],
2680                        self._figi if self._figi else "—",
2681                        instrument["ticker"] if instrument else "—",
2682                        instrument["type"] if instrument else "—",
2683                        item["quantity"] if int(item["quantity"]) > 0 else "—",
2684                        "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—",
2685                        TKS_OPERATION_STATES[item["state"]],
2686                        TKS_OPERATION_TYPES[item["operationType"]],
2687                    ))
2688
2689            infoText = "".join(info)
2690
2691            if show and not onlyFiles:
2692                if self.moreDebug:
2693                    uLogger.debug("Records about history of a client's operations successfully received")
2694
2695                uLogger.info(infoText)
2696
2697            if self.reportFile and (show or onlyFiles):
2698                with open(self.reportFile, "w", encoding="UTF-8") as fH:
2699                    fH.write(infoText)
2700
2701                uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile)))
2702
2703                if self.useHTMLReports:
2704                    htmlFilePath = self.reportFile.replace(".md", ".html") if self.reportFile.endswith(".md") else self.reportFile + ".html"
2705                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
2706                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Client's operations", commonCSS=COMMON_CSS, markdown=infoText))
2707
2708                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
2709
2710        return ops, customStat
2711
2712    def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False, onlyFiles=False) -> pd.DataFrame:
2713        """
2714        This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id).
2715
2716        History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`.
2717        Warning! Broker server used ISO UTC time by default.
2718
2719        If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame.
2720        Also, `historyFile` used to update history with `onlyMissing` parameter.
2721
2722        See also: `LoadHistory()` and `ShowHistoryChart()` methods.
2723
2724        :param start: see docstring in `TradeRoutines.GetDatesAsString()` method.
2725        :param end: see docstring in `TradeRoutines.GetDatesAsString()` method.
2726        :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`,
2727                         `"hour"`, `"day"`. Default: `"hour"`.
2728        :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`.
2729                            False by default. Warning! History appends only from last candle to current time
2730                            with always update last candle!
2731        :param csvSep: separator if csv-file is used, `,` by default.
2732        :param show: if `True` then also prints Pandas DataFrame to the console.
2733        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
2734        :return: Pandas DataFrame with prices history. Headers of columns are defined by default:
2735                 `["date", "time", "open", "high", "low", "close", "volume"]`.
2736        """
2737        strStartDate, strEndDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT)  # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2738        headers = ["date", "time", "open", "high", "low", "close", "volume"]  # sequence and names of column headers
2739        history = None  # empty pandas object for history
2740
2741        if interval not in TKS_CANDLE_INTERVALS.keys():
2742            uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.")
2743            raise Exception("Incorrect value")
2744
2745        if not (self._ticker or self._figi):
2746            uLogger.error("Ticker or FIGI must be defined!")
2747            raise Exception("Ticker or FIGI required")
2748
2749        if self._ticker and not self._figi:
2750            instrumentByTicker = self.SearchByTicker(requestPrice=False)
2751            self._figi = instrumentByTicker["figi"] if instrumentByTicker else ""
2752
2753        if self._figi and not self._ticker:
2754            instrumentByFIGI = self.SearchByFIGI(requestPrice=False)
2755            self._ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else ""
2756
2757        dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from start time string
2758        dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from end time string
2759        if interval.lower() != "day":
2760            dtEnd += timedelta(seconds=1)  # adds 1 sec for requests, because day end returned by `TradeRoutines.GetDatesAsString()` is 23:59:59
2761
2762        delta = dtEnd - dtStart  # current UTC time minus last time in file
2763        deltaMinutes = delta.days * 1440 + delta.seconds // 60  # minutes between start and end dates
2764
2765        # calculate history length in candles:
2766        length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1]
2767        if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0:
2768            length += 1  # to avoid fraction time
2769
2770        # calculate data blocks count:
2771        blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2]
2772
2773        uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self._ticker, self._figi))
2774        if self.moreDebug:
2775            uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end))
2776            uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate))
2777            uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval))
2778            uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2]))
2779
2780        tempOld = None  # pandas object for old history, if --only-missing key present
2781        lastTime = None  # datetime object of last old candle in file
2782
2783        if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile):
2784            if self.moreDebug:
2785                uLogger.debug("--only-missing key present, add only last missing candles...")
2786                uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile)))
2787
2788            tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers)
2789
2790            tempOld["date"] = pd.to_datetime(tempOld["date"])  # load date "as is"
2791            tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d")  # convert date to string
2792            tempOld["time"] = pd.to_datetime(tempOld["time"])  # load time "as is"
2793            tempOld["time"] = tempOld["time"].dt.strftime("%H:%M")  # convert time to string
2794
2795            # get last datetime object from last string in file or minus 1 delta if file is empty:
2796            if len(tempOld) > 0:
2797                lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2798
2799            else:
2800                lastTime = dtEnd - timedelta(days=1)  # history file is empty, so last date set at -1 day
2801
2802            tempOld = tempOld[:-1]  # always remove last old candle because it may be incompletely at the current time
2803
2804        responseJSONs = []  # raw history blocks of data
2805
2806        blockEnd = dtEnd
2807        for item in range(blocks):
2808            tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2]
2809            blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail)
2810
2811            uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format(
2812                item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2813            ))
2814
2815            if blockStart == blockEnd:
2816                uLogger.debug("Skipped this zero-length block...")
2817
2818            else:
2819                # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles
2820                historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles"
2821                self.body = str({
2822                    "figi": self._figi,
2823                    "from": blockStart.strftime(TKS_DATE_TIME_FORMAT),
2824                    "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2825                    "interval": TKS_CANDLE_INTERVALS[interval][0]
2826                })
2827                responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1)
2828
2829                if "code" in responseJSON.keys():
2830                    uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks))
2831
2832                else:
2833                    if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1:
2834                        responseJSON["candles"] = responseJSON["candles"][:-1]  # removes last candle for "yesterday" request
2835
2836                    responseJSONs = responseJSON["candles"] + responseJSONs  # add more old history behind newest dates
2837
2838            blockEnd = blockStart
2839
2840        printCount = len(responseJSONs)  # candles to show in console
2841        if responseJSONs:
2842            tempHistory = pd.DataFrame(
2843                data={
2844                    "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2845                    "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2846                    "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs],
2847                    "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs],
2848                    "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs],
2849                    "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs],
2850                    "volume": [int(item["volume"]) for item in responseJSONs],
2851                },
2852                index=range(len(responseJSONs)),
2853                columns=["date", "time", "open", "high", "low", "close", "volume"],
2854            )
2855            tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d")
2856            tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M")
2857
2858            # append only newest candles to old history if --only-missing key present:
2859            if onlyMissing and tempOld is not None and lastTime is not None:
2860                index = 0  # find start index in tempHistory data:
2861
2862                for i, item in tempHistory.iterrows():
2863                    curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2864
2865                    if curTime == lastTime:
2866                        uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
2867                        index = i
2868                        printCount = index + 1
2869                        break
2870
2871                history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True)
2872
2873            else:
2874                history = tempHistory  # if no `--only-missing` key then load full data from server
2875
2876            if self.moreDebug:
2877                uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False)))
2878
2879        if history is not None and not history.empty:
2880            if show and not onlyFiles:
2881                uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format(
2882                    strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]),
2883                    pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False),
2884                ))
2885
2886        else:
2887            uLogger.warning("Received an empty candles history!")
2888
2889        if self.historyFile is not None:
2890            if history is not None and not history.empty:
2891                history.to_csv(self.historyFile, sep=csvSep, index=False, header=False)
2892                uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self._ticker, self._figi, interval, os.path.abspath(self.historyFile)))
2893
2894            else:
2895                uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile)))
2896
2897        else:
2898            if self.moreDebug:
2899                uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.")
2900
2901        return history
2902
2903    def LoadHistory(self, filePath: str) -> pd.DataFrame:
2904        """
2905        Load candles history from csv-file and return Pandas DataFrame object.
2906
2907        See also: `History()` and `ShowHistoryChart()` methods.
2908
2909        :param filePath: path to csv-file to open.
2910        """
2911        loadedHistory = None  # init candles data object
2912
2913        uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...")
2914
2915        if os.path.exists(filePath):
2916            loadedHistory = self.priceModel.LoadFromFile(filePath)  # load data and get chain of candles as Pandas DataFrame
2917
2918            tfStr = self.priceModel.FormattedDelta(
2919                self.priceModel.timeframe,
2920                "{days} days {hours}h {minutes}m {seconds}s",
2921            ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta(
2922                self.priceModel.timeframe,
2923                "{hours}h {minutes}m {seconds}s",
2924            )
2925
2926            if loadedHistory is not None and not loadedHistory.empty:
2927                uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format(
2928                    len(loadedHistory),
2929                    tfStr,
2930                    pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)),
2931                )
2932
2933            else:
2934                uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath)))
2935
2936        else:
2937            uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath))
2938
2939        return loadedHistory
2940
2941    def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None:
2942        """
2943        Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file.
2944
2945        Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart.
2946        Default: `index.html` (both for interact and non-interact candlesticks chart).
2947
2948        See also: `History()` and `LoadHistory()` methods.
2949
2950        :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object.
2951        :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart.
2952                         See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters
2953                         If False then chain of candlesticks will render as not interactive Google Candlestick chart.
2954                         See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template
2955        :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to
2956                              html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file.
2957        """
2958        if isinstance(candles, str):
2959            self.priceModel.prices = self.LoadHistory(filePath=candles)  # load candles chain from file
2960            self.priceModel.ticker = os.path.basename(candles)  # use filename as ticker name in PriceGenerator
2961
2962        elif isinstance(candles, pd.DataFrame):
2963            self.priceModel.prices = candles  # set candles chain from variable
2964            self.priceModel.ticker = self._ticker  # use current TKSBrokerAPI ticker as ticker name in PriceGenerator
2965
2966            if "datetime" not in candles.columns:
2967                self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True)  # PriceGenerator uses "datetime" column with date and time
2968
2969        else:
2970            uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!")
2971            raise Exception("Incorrect value")
2972
2973        self.priceModel.horizon = len(self.priceModel.prices)  # use length of candles data as horizon in PriceGenerator
2974
2975        if interact:
2976            uLogger.debug("Rendering interactive candles chart. Wait, please...")
2977
2978            self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2979
2980        else:
2981            uLogger.debug("Rendering non-interactive candles chart. Wait, please...")
2982
2983            self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2984
2985        uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile)))
2986
2987    def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2988        """
2989        Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response.
2990        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
2991
2992        See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`.
2993
2994        :param operation: string "Buy" or "Sell".
2995        :param lots: volume, integer count of lots >= 1.
2996        :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`.
2997        :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`.
2998        :param expDate: string "Undefined" by default or local date in future,
2999                        it is a string with format `%Y-%m-%d %H:%M:%S`.
3000        :return: JSON with response from broker server.
3001        """
3002        if self.accountId is None or not self.accountId:
3003            uLogger.error("Variable `accountId` must be defined for using this method!")
3004            raise Exception("Account ID required")
3005
3006        if operation is None or not operation or operation not in ("Buy", "Sell"):
3007            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
3008            raise Exception("Incorrect value")
3009
3010        if lots is None or lots < 1:
3011            uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.")
3012            lots = 1
3013
3014        if tp is None or tp < 0:
3015            tp = 0
3016
3017        if sl is None or sl < 0:
3018            sl = 0
3019
3020        if expDate is None or not expDate:
3021            expDate = "Undefined"
3022
3023        if not (self._ticker or self._figi):
3024            uLogger.error("Ticker or FIGI must be defined!")
3025            raise Exception("Ticker or FIGI required")
3026
3027        instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True)
3028        self._ticker = instrument["ticker"]
3029        self._figi = instrument["figi"]
3030
3031        uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self._ticker, self._figi, lots, tp, sl, expDate))
3032
3033        openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
3034        self.body = str({
3035            "figi": self._figi,
3036            "quantity": str(lots),
3037            "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
3038            "accountId": str(self.accountId),
3039            "orderType": "ORDER_TYPE_MARKET",  # see: TKS_ORDER_TYPES
3040        })
3041        response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0)
3042
3043        if "orderId" in response.keys():
3044            uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format(
3045                operation, response["orderId"],
3046                self._ticker, self._figi, lots,
3047                NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"],
3048                NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"],
3049                NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"],
3050            ))
3051
3052            if tp > 0:
3053                self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate)
3054
3055            if sl > 0:
3056                self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate)
3057
3058        else:
3059            uLogger.warning("Not `oK` status received! Market order not executed. See full debug log and try again open order later.")
3060
3061        return response
3062
3063    def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
3064        """
3065        More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response.
3066        If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter.
3067
3068        See also: `Order()` and `Trade()` docstrings.
3069
3070        :param lots: volume, integer count of lots >= 1.
3071        :param tp: float > 0, take profit price of stop-order.
3072        :param sl: float > 0, stop loss price of stop-order.
3073        :param expDate: it's a local date in future.
3074                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3075        :return: JSON with response from broker server.
3076        """
3077        return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate)
3078
3079    def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
3080        """
3081        More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response.
3082        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
3083
3084        See also: `Order()` and `Trade()` docstrings.
3085
3086        :param lots: volume, integer count of lots >= 1.
3087        :param tp: float > 0, take profit price of stop-order.
3088        :param sl: float > 0, stop loss price of stop-order.
3089        :param expDate: it's a local date in the future.
3090                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3091        :return: JSON with response from broker server.
3092        """
3093        return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate)
3094
3095    def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None:
3096        """
3097        Close position of given instruments.
3098
3099        :param instruments: list of instruments defined by tickers or FIGIs that must be closed.
3100        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
3101                         This avoids unnecessary downloading data from the server.
3102        """
3103        if instruments is None or not instruments:
3104            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
3105            raise Exception("Ticker or FIGI required")
3106
3107        if isinstance(instruments, str):
3108            instruments = [instruments]
3109
3110        uniqueInstruments = self.GetUniqueFIGIs(instruments)
3111        if uniqueInstruments:
3112            if portfolio is None or not portfolio:
3113                portfolio = self.Overview(show=False)
3114
3115            allOpened = [item["figi"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]]
3116            uLogger.debug("All opened instruments by it's FIGI: {}".format(", ".join(allOpened)))
3117
3118            for self._figi in uniqueInstruments:
3119                if self._figi not in allOpened:
3120                    uLogger.warning("Instrument with FIGI [{}] not in open positions list!".format(self._figi))
3121                    continue
3122
3123                # search open trade info about instrument by ticker:
3124                instrument = {}
3125                for iType in TKS_INSTRUMENTS:
3126                    if instrument:
3127                        break
3128
3129                    for item in portfolio["stat"][iType]:
3130                        if item["figi"] == self._figi:
3131                            instrument = item
3132                            break
3133
3134                if instrument:
3135                    self._ticker = instrument["ticker"]
3136                    self._figi = instrument["figi"]
3137
3138                    uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format(
3139                        self._ticker,
3140                        self._figi,
3141                        int(instrument["volume"]),
3142                        ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "",
3143                    ))
3144
3145                    tradeLots = abs(instrument["lots"]) - instrument["blocked"]  # available volumes in lots for close operation
3146
3147                    if tradeLots > 0:
3148                        if instrument["blocked"] > 0:
3149                            uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format(
3150                                instrument["blocked"],
3151                                self._ticker,
3152                                tradeLots,
3153                            ))
3154
3155                        # if direction is "Long" then we need sell, if direction is "Short" then we need buy:
3156                        self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots)
3157
3158                    else:
3159                        uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self._ticker))
3160
3161    def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None:
3162        """
3163        Close all positions of given instruments with defined type.
3164
3165        :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list.
3166        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
3167                         This avoids unnecessary downloading data from the server.
3168        """
3169        if iType not in TKS_INSTRUMENTS:
3170            uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType))
3171
3172        else:
3173            if portfolio is None or not portfolio:
3174                portfolio = self.Overview(show=False)
3175
3176            tickers = [item["ticker"] for item in portfolio["stat"][iType]]
3177            uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers))
3178
3179            if tickers and portfolio:
3180                self.CloseTrades(tickers, portfolio)
3181
3182            else:
3183                uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType))
3184
3185    def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3186        """
3187        Universal method to create market or limit orders with all available parameters for current `accountId`.
3188        See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`.
3189
3190        If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above
3191        current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day.
3192
3193        Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell"
3194        then broker immediately open market order as you can do simple --buy or --sell operations!
3195
3196        If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell".
3197        When current price will go up or down to target price value then broker opens a limit order.
3198        Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter.
3199
3200        Only one attempt and no retry for opens order. If network issue occurred you can create new request.
3201
3202        :param operation: string "Buy" or "Sell".
3203        :param orderType: string "Limit" or "Stop".
3204        :param lots: volume, integer count of lots >= 1.
3205        :param targetPrice: target price > 0. This is open trade price for limit order.
3206        :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice.
3207                           Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order.
3208        :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types
3209                         "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3210                         Stop loss order always executed by market price.
3211        :param expDate: string "Undefined" by default or local date in future.
3212                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3213                        This date is converting to UTC format for server. This parameter only makes sense for stop-order.
3214                        A limit order has no expiration date, it lasts until the end of the trading day.
3215        :return: JSON with response from broker server.
3216        """
3217        if self.accountId is None or not self.accountId:
3218            uLogger.error("Variable `accountId` must be defined for using this method!")
3219            raise Exception("Account ID required")
3220
3221        if operation is None or not operation or operation not in ("Buy", "Sell"):
3222            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
3223            raise Exception("Incorrect value")
3224
3225        if orderType is None or not orderType or orderType not in ("Limit", "Stop"):
3226            uLogger.error("You must define order type only one of them: `Limit` or `Stop`!")
3227            raise Exception("Incorrect value")
3228
3229        if lots is None or lots < 1:
3230            uLogger.error("You must define trade volume > 0: integer count of lots!")
3231            raise Exception("Incorrect value")
3232
3233        if targetPrice is None or targetPrice <= 0:
3234            uLogger.error("Target price for limit-order must be greater than 0!")
3235            raise Exception("Incorrect value")
3236
3237        if limitPrice is None or limitPrice <= 0:
3238            limitPrice = targetPrice
3239
3240        if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"):
3241            stopType = "Limit"
3242
3243        if expDate is None or not expDate:
3244            expDate = "Undefined"
3245
3246        if not (self._ticker or self._figi):
3247            uLogger.error("Tocker or FIGI must be defined!")
3248            raise Exception("Ticker or FIGI required")
3249
3250        response = {}
3251        instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True)
3252        self._ticker = instrument["ticker"]
3253        self._figi = instrument["figi"]
3254
3255        if orderType == "Limit":
3256            uLogger.debug(
3257                "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format(
3258                    self._ticker, self._figi,
3259                    operation, lots, targetPrice, instrument["currency"],
3260                ))
3261
3262            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
3263            self.body = str({
3264                "figi": self._figi,
3265                "quantity": str(lots),
3266                "price": FloatToNano(targetPrice),
3267                "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
3268                "accountId": str(self.accountId),
3269                "orderType": "ORDER_TYPE_LIMIT",  # see: TKS_ORDER_TYPES
3270            })
3271            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0)
3272
3273            if "orderId" in response.keys():
3274                uLogger.info(
3275                    "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{} {}]".format(
3276                        response["orderId"], self._ticker, self._figi, operation, lots,
3277                        "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"],
3278                    ))
3279
3280                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3281                    if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]:
3282                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format(
3283                            targetPrice, instrument["currency"],
3284                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3285                        ))
3286
3287                    if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]:
3288                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format(
3289                            targetPrice, instrument["currency"],
3290                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3291                        ))
3292
3293            else:
3294                uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log and try again open order later.")
3295
3296        if orderType == "Stop":
3297            uLogger.debug(
3298                "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format(
3299                    self._ticker, self._figi,
3300                    operation, lots,
3301                    targetPrice, instrument["currency"],
3302                    limitPrice, instrument["currency"],
3303                    stopType, expDate,
3304                ))
3305
3306            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder"
3307            expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT)
3308            stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT"
3309
3310            body = {
3311                "figi": self._figi,
3312                "quantity": str(lots),
3313                "price": FloatToNano(limitPrice),
3314                "stopPrice": FloatToNano(targetPrice),
3315                "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL",  # see: TKS_STOP_ORDER_DIRECTIONS
3316                "accountId": str(self.accountId),
3317                "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL",  # see: TKS_STOP_ORDER_EXPIRATION_TYPES
3318                "stopOrderType": stopOrderType,  # see: TKS_STOP_ORDER_TYPES
3319            }
3320
3321            if expDateUTC:
3322                body["expireDate"] = expDateUTC
3323
3324            self.body = str(body)
3325            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0)
3326
3327            if "stopOrderId" in response.keys():
3328                uLogger.info(
3329                    "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{} {}], limit price [{} {}], stop-order type [{}] and expiration date [{} UTC]".format(
3330                        response["stopOrderId"], self._ticker, self._figi, operation, lots,
3331                        "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"],
3332                        "{:.4f}".format(limitPrice).rstrip("0").rstrip("."), instrument["currency"],
3333                        TKS_STOP_ORDER_TYPES[stopOrderType],
3334                        datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"],
3335                    ))
3336
3337                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3338                    if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3339                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3340                            targetPrice, instrument["currency"],
3341                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3342                        ))
3343
3344                    if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3345                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3346                            targetPrice, instrument["currency"],
3347                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3348                        ))
3349
3350            else:
3351                uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log and try again open order later.")
3352
3353        return response
3354
3355    def BuyLimit(self, lots: int, targetPrice: float) -> dict:
3356        """
3357        Create pending `Buy` limit-order (below current price). You must specify only 2 parameters:
3358        `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then
3359        broker immediately open `Buy` market order, such as if you do simple `--buy` operation!
3360        See also: `Order()` docstring.
3361
3362        :param lots: volume, integer count of lots >= 1.
3363        :param targetPrice: target price > 0. This is open trade price for limit order.
3364        :return: JSON with response from broker server.
3365        """
3366        return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice)
3367
3368    def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3369        """
3370        Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order.
3371        In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3372        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3373        target price value then broker opens a limit order. See also: `Order()` docstring.
3374
3375        :param lots: volume, integer count of lots >= 1.
3376        :param targetPrice: target price > 0. This is trigger price for buy stop-order.
3377        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3378                           with price equal to limitPrice, when current price goes to target price of buy stop-order.
3379        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3380                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3381        :param expDate: string "Undefined" by default or local date in future.
3382                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3383                        This date is converting to UTC format for server.
3384        :return: JSON with response from broker server.
3385        """
3386        return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
3387
3388    def SellLimit(self, lots: int, targetPrice: float) -> dict:
3389        """
3390        Create pending `Sell` limit-order (above current price). You must specify only 2 parameters:
3391        `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then
3392        broker immediately open `Sell` market order, such as if you do simple `--sell` operation!
3393        See also: `Order()` docstring.
3394
3395        :param lots: volume, integer count of lots >= 1.
3396        :param targetPrice: target price > 0. This is open trade price for limit order.
3397        :return: JSON with response from broker server.
3398        """
3399        return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice)
3400
3401    def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3402        """
3403        Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order.
3404        In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3405        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3406        target price value then broker opens a limit order. See also: `Order()` docstring.
3407
3408        :param lots: volume, integer count of lots >= 1.
3409        :param targetPrice: target price > 0. This is trigger price for sell stop-order.
3410        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3411                           with price equal to limitPrice, when current price goes to target price of sell stop-order.
3412        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3413                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3414        :param expDate: string "Undefined" by default or local date in future.
3415                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3416                        This date is converting to UTC format for server.
3417        :return: JSON with response from broker server.
3418        """
3419        return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
3420
3421    def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None:
3422        """
3423        Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`.
3424
3425        :param orderIDs: list of integers with `orderId` or `stopOrderId`.
3426        :param allOrdersIDs: pre-received lists of all active pending limit orders.
3427                             This avoids unnecessary downloading data from the server.
3428        :param allStopOrdersIDs: pre-received lists of all active stop orders.
3429        """
3430        if self.accountId is None or not self.accountId:
3431            uLogger.error("Variable `accountId` must be defined for using this method!")
3432            raise Exception("Account ID required")
3433
3434        if orderIDs:
3435            if allOrdersIDs is None:
3436                rawOrders = self.RequestPendingOrders()
3437                allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending limit orders ID
3438
3439            if allStopOrdersIDs is None:
3440                rawStopOrders = self.RequestStopOrders()
3441                allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3442
3443            for orderID in orderIDs:
3444                idInPendingOrders = orderID in allOrdersIDs
3445                idInStopOrders = orderID in allStopOrdersIDs
3446
3447                if not (idInPendingOrders or idInStopOrders):
3448                    uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID))
3449                    continue
3450
3451                else:
3452                    if idInPendingOrders:
3453                        uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID))
3454
3455                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder
3456                        self.body = str({"accountId": self.accountId, "orderId": orderID})
3457                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder"
3458                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3459
3460                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3461                            if self.moreDebug:
3462                                uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3463
3464                            uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID))
3465
3466                        else:
3467                            uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID))
3468
3469                    elif idInStopOrders:
3470                        uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID))
3471
3472                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder
3473                        self.body = str({"accountId": self.accountId, "stopOrderId": orderID})
3474                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder"
3475                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3476
3477                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3478                            if self.moreDebug:
3479                                uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3480
3481                            uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID))
3482
3483                        else:
3484                            uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID))
3485
3486                    else:
3487                        continue
3488
3489    def CloseAllOrders(self) -> None:
3490        """
3491        Gets a list of open pending and stop orders and cancel it all.
3492        """
3493        rawOrders = self.RequestPendingOrders()
3494        allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending limit orders ID
3495        lenOrders = len(allOrdersIDs)
3496
3497        rawStopOrders = self.RequestStopOrders()
3498        allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3499        lenSOrders = len(allStopOrdersIDs)
3500
3501        if lenOrders > 0 or lenSOrders > 0:
3502            uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders))
3503
3504            self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs)
3505
3506        else:
3507            uLogger.info("Orders not found, nothing to cancel.")
3508
3509    def CloseAll(self, *args) -> None:
3510        """
3511        Close all available (not blocked) opened trades and orders.
3512
3513        Also, you can select one or more keywords case-insensitive:
3514        `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type.
3515
3516        Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods.
3517        """
3518        overview = self.Overview(show=False)  # get all open trades info
3519
3520        if len(args) == 0:
3521            uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...")
3522            self.CloseAllOrders()  # close all pending and stop orders
3523
3524            for iType in TKS_INSTRUMENTS:
3525                if iType != "Currencies":
3526                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies
3527
3528        else:
3529            uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args)))
3530            lowerArgs = [x.lower() for x in args]
3531
3532            if "orders" in lowerArgs:
3533                self.CloseAllOrders()  # close all pending and stop orders
3534
3535            for iType in TKS_INSTRUMENTS:
3536                if iType.lower() in lowerArgs and iType != "Currencies":
3537                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies
3538
3539    def CloseAllByTicker(self, instrument: str) -> None:
3540        """
3541        Close all available (not blocked) opened trades and orders for one instrument defined by its ticker.
3542
3543        This method searches opened trade and orders of instrument throw all portfolio and then use
3544        `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument.
3545
3546        See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`.
3547
3548        :param instrument: string with ticker.
3549        """
3550        if instrument is None or not instrument:
3551            uLogger.error("Ticker name must be defined for using this method!")
3552            raise Exception("Ticker required")
3553
3554        overview = self.Overview(show=False)  # get user portfolio with all open trades info
3555
3556        self._ticker = instrument  # try to set instrument as ticker
3557        self._figi = ""
3558
3559        limitAll = [item["orderID"] for item in overview["stat"]["orders"]]  # list of all pending limit order IDs
3560        stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]]  # list of all stop order IDs
3561
3562        if limitAll and self.IsInLimitOrders(portfolio=overview):
3563            uLogger.debug("Closing all opened pending limit orders for the instrument with ticker [{}]. Wait, please...")
3564            self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3565
3566        if stopAll and self.IsInStopOrders(portfolio=overview):
3567            uLogger.debug("Closing all opened stop orders for the instrument with ticker [{}]. Wait, please...")
3568            self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3569
3570        if self.IsInPortfolio(portfolio=overview):
3571            uLogger.debug("Closing all available (not blocked) opened trade for the instrument with ticker [{}]. Wait, please...")
3572            self.CloseTrades(instruments=[instrument], portfolio=overview)
3573
3574    def CloseAllByFIGI(self, instrument: str) -> None:
3575        """
3576        Close all available (not blocked) opened trades and orders for one instrument defined by its FIGI id.
3577
3578        This method searches opened trade and orders of instrument throw all portfolio and then use
3579        `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument.
3580
3581        See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`.
3582
3583        :param instrument: string with FIGI id.
3584        """
3585        if instrument is None or not instrument:
3586            uLogger.error("FIGI id must be defined for using this method!")
3587            raise Exception("FIGI required")
3588
3589        overview = self.Overview(show=False)  # get user portfolio with all open trades info
3590
3591        self._ticker = ""
3592        self._figi = instrument  # try to set instrument as FIGI id
3593
3594        limitAll = [item["orderID"] for item in overview["stat"]["orders"]]  # list of all pending limit order IDs
3595        stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]]  # list of all stop order IDs
3596
3597        if limitAll and self.IsInLimitOrders(portfolio=overview):
3598            uLogger.debug("Closing all opened pending limit orders for the instrument with FIGI [{}]. Wait, please...")
3599            self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3600
3601        if stopAll and self.IsInStopOrders(portfolio=overview):
3602            uLogger.debug("Closing all opened stop orders for the instrument with FIGI [{}]. Wait, please...")
3603            self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3604
3605        if self.IsInPortfolio(portfolio=overview):
3606            uLogger.debug("Closing all available (not blocked) opened trade for the instrument with FIGI [{}]. Wait, please...")
3607            self.CloseTrades(instruments=[instrument], portfolio=overview)
3608
3609    @staticmethod
3610    def ParseOrderParameters(operation, **inputParameters):
3611        """
3612        Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders.
3613
3614        :param operation: string "Buy" or "Sell".
3615        :param inputParameters: this is dict of strings that looks like this
3616               `{"lots": "L_int,...", "prices": "P_float,..."}` where
3617               "lots" key: one or more lot values (integer numbers) to open with every limit-order
3618               "prices" key: one or more prices to open limit-orders
3619               Counts of values in lots and prices lists must be equals!
3620        :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]`
3621        """
3622        # TODO: update order grid work with api v2
3623        pass
3624        # uLogger.debug("Input parameters: {}".format(inputParameters))
3625        #
3626        # if operation is None or not operation or operation not in ("Buy", "Sell"):
3627        #     uLogger.error("You must define operation type: 'Buy' or 'Sell'!")
3628        #     raise Exception("Incorrect value")
3629        #
3630        # if "l" in inputParameters.keys():
3631        #     inputParameters["lots"] = inputParameters.pop("l")
3632        #
3633        # if "p" in inputParameters.keys():
3634        #     inputParameters["prices"] = inputParameters.pop("p")
3635        #
3636        # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys():
3637        #     uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!")
3638        #     raise Exception("Incorrect value")
3639        #
3640        # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")]
3641        # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")]
3642        #
3643        # if len(lots) != len(prices):
3644        #     uLogger.error("'lots' and 'prices' lists must have equal length of values!")
3645        #     raise Exception("Incorrect value")
3646        #
3647        # uLogger.debug("Extracted parameters for orders:")
3648        # uLogger.debug("lots = {}".format(lots))
3649        # uLogger.debug("prices = {}".format(prices))
3650        #
3651        # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...]
3652        # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))]
3653        # uLogger.debug("Order parameters: {}".format(result))
3654        #
3655        # return result
3656
3657    def IsInPortfolio(self, portfolio: dict = None) -> bool:
3658        """
3659        Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`.
3660
3661        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3662        :return: `True` if portfolio contains open position with given instrument, `False` otherwise.
3663        """
3664        result = False
3665        msg = "Instrument not defined!"
3666
3667        if portfolio is None or not portfolio:
3668            portfolio = self.Overview(show=False)
3669
3670        if self._ticker:
3671            uLogger.debug("Searching instrument with ticker [{}] throw opened positions list...".format(self._ticker))
3672            msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker)
3673
3674            for iType in TKS_INSTRUMENTS:
3675                for instrument in portfolio["stat"][iType]:
3676                    if instrument["ticker"] == self._ticker:
3677                        result = True
3678                        msg = "Instrument with ticker [{}] is present in open positions".format(self._ticker)
3679                        break
3680
3681        elif self._figi:
3682            uLogger.debug("Searching instrument with FIGI [{}] throw opened positions list...".format(self._figi))
3683            msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi)
3684
3685            for iType in TKS_INSTRUMENTS:
3686                for instrument in portfolio["stat"][iType]:
3687                    if instrument["figi"] == self._figi:
3688                        result = True
3689                        msg = "Instrument with FIGI [{}] is present in open positions".format(self._figi)
3690                        break
3691
3692        else:
3693            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3694
3695        uLogger.debug(msg)
3696
3697        return result
3698
3699    def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict:
3700        """
3701        Returns instrument from the user's portfolio if it presents there.
3702        Instrument must be defined by `ticker` (highly priority) or `figi`.
3703
3704        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3705        :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise.
3706        """
3707        result = None
3708        msg = "Instrument not defined!"
3709
3710        if portfolio is None or not portfolio:
3711            portfolio = self.Overview(show=False)
3712
3713        if self._ticker:
3714            uLogger.debug("Searching instrument with ticker [{}] in opened positions...".format(self._ticker))
3715            msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker)
3716
3717            for iType in TKS_INSTRUMENTS:
3718                for instrument in portfolio["stat"][iType]:
3719                    if instrument["ticker"] == self._ticker:
3720                        result = instrument
3721                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self._ticker, instrument["figi"])
3722                        break
3723
3724        elif self._figi:
3725            uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self._figi))
3726            msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi)
3727
3728            for iType in TKS_INSTRUMENTS:
3729                for instrument in portfolio["stat"][iType]:
3730                    if instrument["figi"] == self._figi:
3731                        result = instrument
3732                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self._figi)
3733                        break
3734
3735        else:
3736            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3737
3738        uLogger.debug(msg)
3739
3740        return result
3741
3742    def IsInLimitOrders(self, portfolio: dict = None) -> bool:
3743        """
3744        Checks if instrument is in the limit orders list. Instrument must be defined by `ticker` (highly priority) or `figi`.
3745
3746        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3747
3748        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3749        :return: `True` if limit orders list contains some limit orders for the instrument, `False` otherwise.
3750        """
3751        result = False
3752        msg = "Instrument not defined!"
3753
3754        if portfolio is None or not portfolio:
3755            portfolio = self.Overview(show=False)
3756
3757        if self._ticker:
3758            uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker))
3759            msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker)
3760
3761            for instrument in portfolio["stat"]["orders"]:
3762                if instrument["ticker"] == self._ticker:
3763                    result = True
3764                    msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker)
3765                    break
3766
3767        elif self._figi:
3768            uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi))
3769            msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi)
3770
3771            for instrument in portfolio["stat"]["orders"]:
3772                if instrument["figi"] == self._figi:
3773                    result = True
3774                    msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi)
3775                    break
3776
3777        else:
3778            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3779
3780        uLogger.debug(msg)
3781
3782        return result
3783
3784    def GetLimitOrderIDs(self, portfolio: dict = None) -> list[str]:
3785        """
3786        Returns list with all `orderID`s of opened pending limit orders for the instrument.
3787        Instrument must be defined by `ticker` (highly priority) or `figi`.
3788
3789        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3790
3791        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3792        :return: list with `orderID`s of limit orders.
3793        """
3794        result = []
3795        msg = "Instrument not defined!"
3796
3797        if portfolio is None or not portfolio:
3798            portfolio = self.Overview(show=False)
3799
3800        if self._ticker:
3801            uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker))
3802            msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker)
3803
3804            for instrument in portfolio["stat"]["orders"]:
3805                if instrument["ticker"] == self._ticker:
3806                    result.append(instrument["orderID"])
3807
3808            if result:
3809                msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker)
3810
3811        elif self._figi:
3812            uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi))
3813            msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi)
3814
3815            for instrument in portfolio["stat"]["orders"]:
3816                if instrument["figi"] == self._figi:
3817                    result.append(instrument["orderID"])
3818
3819            if result:
3820                msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi)
3821
3822        else:
3823            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3824
3825        uLogger.debug(msg)
3826
3827        return result
3828
3829    def IsInStopOrders(self, portfolio: dict = None) -> bool:
3830        """
3831        Checks if instrument is in the stop orders list. Instrument must be defined by `ticker` (highly priority) or `figi`.
3832
3833        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3834
3835        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3836        :return: `True` if stop orders list contains some stop orders for the instrument, `False` otherwise.
3837        """
3838        result = False
3839        msg = "Instrument not defined!"
3840
3841        if portfolio is None or not portfolio:
3842            portfolio = self.Overview(show=False)
3843
3844        if self._ticker:
3845            uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker))
3846            msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker)
3847
3848            for instrument in portfolio["stat"]["stopOrders"]:
3849                if instrument["ticker"] == self._ticker:
3850                    result = True
3851                    msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker)
3852                    break
3853
3854        elif self._figi:
3855            uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi))
3856            msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi)
3857
3858            for instrument in portfolio["stat"]["stopOrders"]:
3859                if instrument["figi"] == self._figi:
3860                    result = True
3861                    msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi)
3862                    break
3863
3864        else:
3865            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3866
3867        uLogger.debug(msg)
3868
3869        return result
3870
3871    def GetStopOrderIDs(self, portfolio: dict = None) -> list[str]:
3872        """
3873        Returns list with all `orderID`s of opened stop orders for the instrument.
3874        Instrument must be defined by `ticker` (highly priority) or `figi`.
3875
3876        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3877
3878        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3879        :return: list with `orderID`s of stop orders.
3880        """
3881        result = []
3882        msg = "Instrument not defined!"
3883
3884        if portfolio is None or not portfolio:
3885            portfolio = self.Overview(show=False)
3886
3887        if self._ticker:
3888            uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker))
3889            msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker)
3890
3891            for instrument in portfolio["stat"]["stopOrders"]:
3892                if instrument["ticker"] == self._ticker:
3893                    result.append(instrument["orderID"])
3894
3895            if result:
3896                msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker)
3897
3898        elif self._figi:
3899            uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi))
3900            msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi)
3901
3902            for instrument in portfolio["stat"]["stopOrders"]:
3903                if instrument["figi"] == self._figi:
3904                    result.append(instrument["orderID"])
3905
3906            if result:
3907                msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi)
3908
3909        else:
3910            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3911
3912        uLogger.debug(msg)
3913
3914        return result
3915
3916    def RequestLimits(self) -> dict:
3917        """
3918        Method for obtaining the available funds for withdrawal for current `accountId`.
3919
3920        See also:
3921        - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits
3922        - `OverviewLimits()` method
3923
3924        :return: dict with raw data from server that contains free funds for withdrawal. Example of dict:
3925                 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`.
3926                 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency
3927                 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures.
3928        """
3929        if self.accountId is None or not self.accountId:
3930            uLogger.error("Variable `accountId` must be defined for using this method!")
3931            raise Exception("Account ID required")
3932
3933        uLogger.debug("Requesting current available funds for withdrawal. Wait, please...")
3934
3935        self.body = str({"accountId": self.accountId})
3936        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits"
3937        rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
3938
3939        if self.moreDebug:
3940            uLogger.debug("Records about available funds for withdrawal successfully received")
3941
3942        return rawLimits
3943
3944    def OverviewLimits(self, show: bool = False, onlyFiles=False) -> dict:
3945        """
3946        Method for parsing and show table with available funds for withdrawal for current `accountId`.
3947
3948        See also: `RequestLimits()`.
3949
3950        :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log.
3951        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
3952        :return: dict with raw parsed data from server and some calculated statistics about it.
3953        """
3954        if self.accountId is None or not self.accountId:
3955            uLogger.error("Variable `accountId` must be defined for using this method!")
3956            raise Exception("Account ID required")
3957
3958        rawLimits = self.RequestLimits()  # raw response with current available funds for withdrawal
3959
3960        view = {
3961            "rawLimits": rawLimits,
3962            "limits": {  # parsed data for every currency:
3963                "money": {  # this is an array of portfolio currency positions
3964                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"]
3965                },
3966                "blocked": {  # this is an array of blocked currency
3967                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"]
3968                },
3969                "blockedGuarantee": {  # this is locked money under collateral for futures
3970                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"]
3971                },
3972            },
3973        }
3974
3975        # --- Prepare text table with limits in human-readable format:
3976        if show or onlyFiles:
3977            info = [
3978                "# Withdrawal limits\n\n",
3979                "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
3980                "* **Account ID:** [{}]\n".format(self.accountId),
3981            ]
3982
3983            if view["limits"]["money"]:
3984                info.extend([
3985                    "\n| Currencies | Total         | Available for withdrawal | Blocked for trade | Futures guarantee |\n",
3986                    "|------------|---------------|--------------------------|-------------------|-------------------|\n",
3987                ])
3988
3989            else:
3990                info.append("\nNo withdrawal limits\n")
3991
3992            for curr in view["limits"]["money"].keys():
3993                blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0
3994                blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0
3995                availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee)
3996
3997                infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format(
3998                    "[{}]".format(curr),
3999                    "{:.2f}".format(view["limits"]["money"][curr]),
4000                    "{:.2f}".format(availableMoney),
4001                    "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—",
4002                    "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—",
4003                )
4004
4005                if curr == "rub":
4006                    info.insert(5, infoStr)  # hack: insert "rub" at the first position in table and after headers
4007
4008                else:
4009                    info.append(infoStr)
4010
4011            infoText = "".join(info)
4012
4013            if show and not onlyFiles:
4014                uLogger.info(infoText)
4015
4016            if self.withdrawalLimitsFile and (show or onlyFiles):
4017                with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH:
4018                    fH.write(infoText)
4019
4020                uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile)))
4021
4022                if self.useHTMLReports:
4023                    htmlFilePath = self.withdrawalLimitsFile.replace(".md", ".html") if self.withdrawalLimitsFile.endswith(".md") else self.withdrawalLimitsFile + ".html"
4024                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
4025                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Withdrawal limits", commonCSS=COMMON_CSS, markdown=infoText))
4026
4027                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
4028
4029        return view
4030
4031    def RequestAccounts(self) -> dict:
4032        """
4033        Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`.
4034
4035        See also:
4036        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts
4037        - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account
4038        - `OverviewUserInfo()` method
4039
4040        :return: dict with raw data from server that contains accounts info. Example of dict:
4041                 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account",
4042                   "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z",
4043                   "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`.
4044                 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now.
4045        """
4046        uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...")
4047
4048        self.body = str({})
4049        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts"
4050        rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST")
4051
4052        if self.moreDebug:
4053            uLogger.debug("Records about available accounts successfully received")
4054
4055        return rawAccounts
4056
4057    def RequestUserInfo(self) -> dict:
4058        """
4059        Method for requesting common user's information.
4060
4061        See also:
4062        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo
4063        - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest
4064        - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with
4065        - `OverviewUserInfo()` method
4066
4067        :return: dict with raw data from server that contains user's information. Example of dict:
4068                 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage",
4069                   "russian_shares", "structured_income_bonds"], "tariff": "premium"}`.
4070        """
4071        uLogger.debug("Requesting common user's information. Wait, please...")
4072
4073        self.body = str({})
4074        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo"
4075        rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST")
4076
4077        if self.moreDebug:
4078            uLogger.debug("Records about current user successfully received")
4079
4080        return rawUserInfo
4081
4082    def RequestMarginStatus(self, accountId: str = None) -> dict:
4083        """
4084        Method for requesting margin calculation for defined account ID.
4085
4086        See also:
4087        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes
4088        - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse
4089        - `OverviewUserInfo()` method
4090
4091        :param accountId: string with numeric account ID. If `None`, then used class field `accountId`.
4092        :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict.
4093                 Example of responses:
4094                 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`.
4095                 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000},
4096                                    "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000},
4097                                    "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000},
4098                                    "fundsSufficiencyLevel": {"units": "1", "nano": 280000000},
4099                                    "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`.
4100        """
4101        if accountId is None or not accountId:
4102            if self.accountId is None or not self.accountId:
4103                uLogger.error("Variable `accountId` must be defined for using this method!")
4104                raise Exception("Account ID required")
4105
4106            else:
4107                accountId = self.accountId  # use `self.accountId` (main ID) by default
4108
4109        uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId))
4110
4111        self.body = str({"accountId": accountId})
4112        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes"
4113        rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST")
4114
4115        if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}:
4116            uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId))
4117            rawMargin = {}
4118
4119        else:
4120            if self.moreDebug:
4121                uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId))
4122
4123        return rawMargin
4124
4125    def RequestTariffLimits(self) -> dict:
4126        """
4127        Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`.
4128
4129        See also:
4130        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff
4131        - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest
4132        - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit
4133        - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit
4134        - `OverviewUserInfo()` method
4135
4136        :return: dict with raw data from server that contains limits of current tariff. Example of dict:
4137                 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...],
4138                   "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`.
4139        """
4140        uLogger.debug("Requesting limits of current tariff. Wait, please...")
4141
4142        self.body = str({})
4143        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff"
4144        rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
4145
4146        if self.moreDebug:
4147            uLogger.debug("Records with limits of current tariff successfully received")
4148
4149        return rawTariffLimits
4150
4151    def RequestBondCoupons(self, iJSON: dict) -> dict:
4152        """
4153        Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown
4154        then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`.
4155        All dates are in UTC timezone.
4156
4157        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons
4158        Documentation:
4159        - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest
4160        - response: https://tinkoff.github.io/investAPI/instruments/#coupon
4161
4162        See also: `ExtendBondsData()`.
4163
4164        :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self._ticker]`
4165                      If raw iJSON is not data of bond then server returns an error [400] with message:
4166                      `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`.
4167        :return: dictionary with bond payment calendar. Response example
4168                 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12",
4169                   "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000},
4170                   "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z",
4171                   "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}`
4172        """
4173        if iJSON["figi"] is None or not iJSON["figi"]:
4174            uLogger.error("FIGI must be defined for using this method!")
4175            raise Exception("FIGI required")
4176
4177        startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z"
4178        endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z"
4179
4180        uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format(
4181            "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "",
4182            self._figi,
4183            startDate,
4184            endDate,
4185        ))
4186
4187        self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate})
4188        calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons"
4189        calendar = self.SendAPIRequest(calendarURL, reqType="POST")
4190
4191        if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}:
4192            uLogger.warning("Instrument type is not bond!")
4193
4194        else:
4195            if self.moreDebug:
4196                uLogger.debug("Records about bond payment calendar successfully received")
4197
4198        return calendar
4199
4200    def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame:
4201        """
4202        Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider
4203        Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar,
4204        coupon yields, current yields and some statistics etc.
4205
4206        WARNING! This is too long operation if a lot of bonds requested from broker server.
4207
4208        See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`.
4209
4210        :param instruments: list of strings with tickers or FIGIs.
4211        :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`,
4212                     for further used by data scientists or stock analytics.
4213        :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker.
4214                 In XLSX-file and Pandas DataFrame fields mean:
4215                 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond
4216                 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon
4217        """
4218        if instruments is None or not instruments:
4219            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
4220            raise Exception("Ticker or FIGI required")
4221
4222        if isinstance(instruments, str):
4223            instruments = [instruments]
4224
4225        uniqueInstruments = self.GetUniqueFIGIs(instruments)
4226
4227        uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...")
4228
4229        iCount = len(uniqueInstruments)
4230        tooLong = iCount >= 20
4231        if tooLong:
4232            uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...")
4233
4234        bonds = None
4235        for i, self._figi in enumerate(uniqueInstruments):
4236            instrument = self.SearchByFIGI(requestPrice=False)  # raw data about instrument from server
4237
4238            if "type" in instrument.keys() and instrument["type"] == "Bonds":
4239                # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond
4240                rawBond = self.SearchByFIGI(requestPrice=True)
4241
4242                # Widen raw data with UTC current time (iData["actualDateTime"]):
4243                actualDate = datetime.now(tzutc())
4244                iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond
4245
4246                # Widen raw data with bond payment calendar (iData["rawCalendar"]):
4247                iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)}
4248
4249                # Replace some values with human-readable:
4250                iData["nominalCurrency"] = iData["nominal"]["currency"]
4251                iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"])
4252                iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"])
4253                iData["aciCurrency"] = iData["aciValue"]["currency"]
4254                iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"])
4255                iData["issueSize"] = int(iData["issueSize"])
4256                iData["issueSizePlan"] = int(iData["issueSizePlan"])
4257                iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]]
4258                iData["step"] = iData["step"] if "step" in iData.keys() else 0
4259                iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]]
4260                iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0
4261                iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0
4262                iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0
4263                iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0
4264                iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0
4265                iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0
4266
4267                # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date):
4268                iData["limitUpPercent"] = iData["currentPrice"]["limitUp"]  # max price on current day in percents of nominal
4269                iData["limitDownPercent"] = iData["currentPrice"]["limitDown"]  # min price on current day in percents of nominal
4270                iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"]  # last price on market in percents of nominal
4271                iData["closePricePercent"] = iData["currentPrice"]["closePrice"]  # previous day close in percents of nominal
4272                iData["changes"] = iData["currentPrice"]["changes"]  # this is percent of changes between `currentPrice` and `lastPrice`
4273                iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100  # max price on current day is `limitUpPercent` * `nominal`
4274                iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100  # min price on current day is `limitDownPercent` * `nominal`
4275                iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100  # last price on market is `lastPricePercent` * `nominal`
4276                iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100  # previous day close is `closePricePercent` * `nominal`
4277                iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"]  # this is delta between last deal price and last close
4278
4279                # Widen raw data with calendar data from `rawCalendar` values:
4280                calendarData = []
4281                if "events" in iData["rawCalendar"].keys():
4282                    for item in iData["rawCalendar"]["events"]:
4283                        calendarData.append({
4284                            "couponDate": item["couponDate"],
4285                            "couponNumber": int(item["couponNumber"]),
4286                            "fixDate": item["fixDate"] if "fixDate" in item.keys() else "",
4287                            "payCurrency": item["payOneBond"]["currency"],
4288                            "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]),
4289                            "couponType": TKS_COUPON_TYPES[item["couponType"]],
4290                            "couponStartDate": item["couponStartDate"],
4291                            "couponEndDate": item["couponEndDate"],
4292                            "couponPeriod": item["couponPeriod"],
4293                        })
4294
4295                    # if maturity date is unknown then uses the latest date in bond payment calendar for it:
4296                    if "maturityDate" not in iData.keys():
4297                        iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else ""
4298
4299                # Widen raw data with Coupon Rate.
4300                # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%:
4301                iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData])
4302                iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData])
4303                iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0.
4304
4305                # Widen raw data with Yield to Maturity (YTM) on current date.
4306                # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%:
4307                maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None
4308                iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None
4309                iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate])
4310                iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"]  # sum of all last coupons minus current ACI value
4311                iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0.
4312
4313                iData["calendar"] = calendarData  # adds calendar at the end
4314
4315                # Remove not used data:
4316                iData.pop("uid")
4317                iData.pop("positionUid")
4318                iData.pop("currentPrice")
4319                iData.pop("rawCalendar")
4320
4321                colNames = list(iData.keys())
4322                if bonds is None:
4323                    bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames))
4324
4325                else:
4326                    bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True)
4327
4328            else:
4329                uLogger.warning("Instrument is not a bond!")
4330
4331            processed = round(100 * (i + 1) / iCount, 1)
4332            if tooLong and processed % 5 == 0:
4333                uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount))
4334
4335            else:
4336                uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount))
4337
4338        bonds.index = bonds["ticker"].tolist()  # replace indexes with ticker names
4339
4340        # Saving bonds from Pandas DataFrame to XLSX sheet:
4341        if xlsx and self.bondsXLSXFile:
4342            with pd.ExcelWriter(
4343                    path=self.bondsXLSXFile,
4344                    date_format=TKS_DATE_FORMAT,
4345                    datetime_format=TKS_DATE_TIME_FORMAT,
4346                    mode="w",
4347            ) as writer:
4348                bonds.to_excel(
4349                    writer,
4350                    sheet_name="Extended bonds data",
4351                    index=True,
4352                    encoding="UTF-8",
4353                    freeze_panes=(1, 1),
4354                )  # saving as XLSX-file with freeze first row and column as headers
4355
4356            uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile)))
4357
4358        return bonds
4359
4360    def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame:
4361        """
4362        Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default.
4363
4364        WARNING! This is too long operation if a lot of bonds requested from broker server.
4365
4366        See also: `ShowBondsCalendar()`, `ExtendBondsData()`.
4367
4368        :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
4369                        extended information about bonds: main info, current prices, bond payment calendar,
4370                        coupon yields, current yields and some statistics etc.
4371                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
4372        :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default,
4373                     for further used by data scientists or stock analytics.
4374        :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon
4375        """
4376        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4377            extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=False)
4378
4379        uLogger.debug("Generating bond payments calendar data. Wait, please...")
4380
4381        colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"]
4382        colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"]
4383        calendar = None
4384        for bond in extBonds.iterrows():
4385            for item in bond[1]["calendar"]:
4386                cData = {
4387                    "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()),
4388                    "couponDate": item["couponDate"],
4389                    "figi": bond[1]["figi"],
4390                    "ticker": bond[1]["ticker"],
4391                    "name": bond[1]["name"],
4392                    "couponNumber": item["couponNumber"],
4393                    "payOneBond": item["payOneBond"],
4394                    "payCurrency": item["payCurrency"],
4395                    "couponType": item["couponType"],
4396                    "couponPeriod": item["couponPeriod"],
4397                    "fixDate": item["fixDate"],
4398                    "couponStartDate": item["couponStartDate"],
4399                    "couponEndDate": item["couponEndDate"],
4400                }
4401
4402                if calendar is None:
4403                    calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID))
4404
4405                else:
4406                    calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True)
4407
4408        if calendar is not None:
4409            calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True)  # sort all payments for all bonds by payment date
4410
4411            # Saving calendar from Pandas DataFrame to XLSX sheet:
4412            if xlsx:
4413                xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx"
4414
4415                with pd.ExcelWriter(
4416                        path=xlsxCalendarFile,
4417                        date_format=TKS_DATE_FORMAT,
4418                        datetime_format=TKS_DATE_TIME_FORMAT,
4419                        mode="w",
4420                ) as writer:
4421                    humanReadable = calendar.copy(deep=True)
4422                    humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0])
4423                    humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0])
4424                    humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0])
4425                    humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0])
4426                    humanReadable.columns = colNames  # human-readable column names
4427
4428                    humanReadable.to_excel(
4429                        writer,
4430                        sheet_name="Bond payments calendar",
4431                        index=False,
4432                        encoding="UTF-8",
4433                        freeze_panes=(1, 2),
4434                    )  # saving as XLSX-file with freeze first row and column as headers
4435
4436                    del humanReadable  # release df in memory
4437
4438                uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile)))
4439
4440        return calendar
4441
4442    def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True, onlyFiles=False) -> str:
4443        """
4444        Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond.
4445        Also, creates Markdown file with calendar data, `calendar.md` by default.
4446
4447        See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`.
4448
4449        :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
4450                        extended information about bonds: main info, current prices, bond payment calendar,
4451                        coupon yields, current yields and some statistics etc.
4452                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
4453        :param show: if `True` then also printing bonds payment calendar to the console,
4454                     otherwise save to file `calendarFile` only. `False` by default.
4455        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
4456        :return: multilines text in Markdown format with bonds payment calendar as a table.
4457        """
4458        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4459            extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=show or onlyFiles)
4460
4461        infoText = "# Bond payments calendar\n\n"
4462
4463        calendar = self.CreateBondsCalendar(extBonds, xlsx=show or onlyFiles)  # generate Pandas DataFrame with full calendar data
4464
4465        if not (calendar is None or calendar.empty):
4466            splitLine = "|       |                 |              |              |     |               |           |        |                   |\n"
4467
4468            info = [
4469                "* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4470                "| Paid  | Payment date    | FIGI         | Ticker       | No. | Value         | Type      | Period | End registry date |\n",
4471                "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n",
4472            ]
4473
4474            newMonth = False
4475            notOneBond = calendar["figi"].nunique() > 1
4476            for i, bond in enumerate(calendar.iterrows()):
4477                if newMonth and notOneBond:
4478                    info.append(splitLine)
4479
4480                info.append(
4481                    "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format(
4482                        "  √" if bond[1]["paid"] else "  —",
4483                        bond[1]["couponDate"].split("T")[0],
4484                        bond[1]["figi"],
4485                        bond[1]["ticker"],
4486                        bond[1]["couponNumber"],
4487                        "{} {}".format(
4488                            "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."),
4489                            bond[1]["payCurrency"],
4490                        ),
4491                        bond[1]["couponType"],
4492                        bond[1]["couponPeriod"],
4493                        bond[1]["fixDate"].split("T")[0],
4494                    )
4495                )
4496
4497                if i < len(calendar.values) - 1:
4498                    curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4499                    nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4500                    newMonth = False if curDate.month == nextDate.month else True
4501
4502                else:
4503                    newMonth = False
4504
4505            infoText += "".join(info)
4506
4507            if show and not onlyFiles:
4508                uLogger.info("{}".format(infoText))
4509
4510            if self.calendarFile is not None and (show or onlyFiles):
4511                with open(self.calendarFile, "w", encoding="UTF-8") as fH:
4512                    fH.write(infoText)
4513
4514                uLogger.info("Bond payments calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile)))
4515
4516                if self.useHTMLReports:
4517                    htmlFilePath = self.calendarFile.replace(".md", ".html") if self.calendarFile.endswith(".md") else self.calendarFile + ".html"
4518                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
4519                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Bond payments calendar", commonCSS=COMMON_CSS, markdown=infoText))
4520
4521                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
4522
4523        else:
4524            infoText += "No data\n"
4525
4526        return infoText
4527
4528    def OverviewAccounts(self, show: bool = False, onlyFiles=False) -> dict:
4529        """
4530        Method for parsing and show simple table with all available user accounts.
4531
4532        See also: `RequestAccounts()` and `OverviewUserInfo()` methods.
4533
4534        :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log.
4535        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
4536        :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict:
4537                 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...},
4538                          "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1",
4539                                                        "status": "Opened and active account", "opened": "2018-05-23 00:00:00",
4540                                                        "closed": "—", "access": "Full access" }, ...}}`
4541        """
4542        rawAccounts = self.RequestAccounts()  # Raw responses with accounts
4543
4544        # This is an array of dict with user accounts, its `accountId`s and some parsed data:
4545        accounts = {
4546            item["id"]: {
4547                "type": TKS_ACCOUNT_TYPES[item["type"]],
4548                "name": item["name"],
4549                "status": TKS_ACCOUNT_STATUSES[item["status"]],
4550                "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4551                "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—",
4552                "access": TKS_ACCESS_LEVELS[item["accessLevel"]],
4553            } for item in rawAccounts["accounts"]
4554        }
4555
4556        # Raw and parsed data with some fields replaced in "stat" section:
4557        view = {
4558            "rawAccounts": rawAccounts,
4559            "stat": accounts,
4560        }
4561
4562        # --- Prepare simple text table with only accounts data in human-readable format:
4563        if show or onlyFiles:
4564            info = [
4565                "# User accounts\n\n",
4566                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4567                "| Account ID   | Type                      | Status                    | Name                           |\n",
4568                "|--------------|---------------------------|---------------------------|--------------------------------|\n",
4569            ]
4570
4571            for account in view["stat"].keys():
4572                info.extend([
4573                    "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format(
4574                        account,
4575                        view["stat"][account]["type"],
4576                        view["stat"][account]["status"],
4577                        view["stat"][account]["name"],
4578                    )
4579                ])
4580
4581            infoText = "".join(info)
4582
4583            if show and not onlyFiles:
4584                uLogger.info(infoText)
4585
4586            if self.userAccountsFile and (show or onlyFiles):
4587                with open(self.userAccountsFile, "w", encoding="UTF-8") as fH:
4588                    fH.write(infoText)
4589
4590                uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile)))
4591
4592                if self.useHTMLReports:
4593                    htmlFilePath = self.userAccountsFile.replace(".md", ".html") if self.userAccountsFile.endswith(".md") else self.userAccountsFile + ".html"
4594                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
4595                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User accounts", commonCSS=COMMON_CSS, markdown=infoText))
4596
4597                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
4598
4599        return view
4600
4601    def OverviewUserInfo(self, show: bool = False, onlyFiles=False) -> dict:
4602        """
4603        Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit).
4604
4605        See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods.
4606
4607        :param show: if `False` then only dictionary returns, if `True` then also print user's data to log.
4608        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
4609        :return: dict with raw parsed data from server and some calculated statistics about it.
4610        """
4611        overview = self.Overview(show=False)  # Request current user portfolio for the ability to calculate missing funds
4612        tmpTicker = self._ticker
4613        self._ticker = "RUB000UTSTOM"  # This instrument show in rub how much money cost current margin
4614        missing = self.GetInstrumentFromPortfolio(portfolio=overview)
4615        self._ticker = tmpTicker
4616
4617        rawUserInfo = self.RequestUserInfo()  # Raw response with common user info
4618        overviewAccount = self.OverviewAccounts(show=False)  # Raw and parsed accounts data
4619        rawAccounts = overviewAccount["rawAccounts"]  # Raw response with user accounts data
4620        accounts = overviewAccount["stat"]  # Dict with only statistics about user accounts
4621        rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()}  # Raw response with margin calculation for every account ID
4622        rawTariffLimits = self.RequestTariffLimits()  # Raw response with limits of current tariff
4623
4624        # This is dict with parsed common user data:
4625        userInfo = {
4626            "premium": "Yes" if rawUserInfo["premStatus"] else "No",
4627            "qualified": "Yes" if rawUserInfo["qualStatus"] else "No",
4628            "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]],
4629            "tariff": rawUserInfo["tariff"],
4630        }
4631
4632        # This is an array of dict with parsed margin statuses for every account IDs:
4633        margins = {}
4634        for accountId in accounts.keys():
4635            if rawMargins[accountId]:
4636                margins[accountId] = {
4637                    "currency": rawMargins[accountId]["liquidPortfolio"]["currency"],
4638                    "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]),
4639                    "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]),
4640                    "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]),
4641                    "diff": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]),
4642                    "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]),
4643                    "missing": missing["volume"],
4644                }
4645
4646            else:
4647                margins[accountId] = {}  # Server response: margin status is disabled for current accountId
4648
4649        unary = {}  # unary-connection limits
4650        for item in rawTariffLimits["unaryLimits"]:
4651            if item["limitPerMinute"] in unary.keys():
4652                unary[item["limitPerMinute"]].extend(item["methods"])
4653
4654            else:
4655                unary[item["limitPerMinute"]] = item["methods"]
4656
4657        stream = {}  # stream-connection limits
4658        for item in rawTariffLimits["streamLimits"]:
4659            if item["limit"] in stream.keys():
4660                stream[item["limit"]].extend(item["streams"])
4661
4662            else:
4663                stream[item["limit"]] = item["streams"]
4664
4665        # This is dict with parsed limits of current tariff (connections, API methods etc.):
4666        limits = {
4667            "unary": unary,
4668            "stream": stream,
4669        }
4670
4671        # Raw and parsed data as an output result:
4672        view = {
4673            "rawUserInfo": rawUserInfo,
4674            "rawAccounts": rawAccounts,
4675            "rawMargins": rawMargins,
4676            "rawTariffLimits": rawTariffLimits,
4677            "stat": {
4678                "overview": overview,
4679                "userInfo": userInfo,
4680                "accounts": accounts,
4681                "margins": margins,
4682                "limits": limits,
4683            },
4684        }
4685
4686        # --- Prepare text table with user information in human-readable format:
4687        if show or onlyFiles:
4688            info = [
4689                "# Full user information\n\n",
4690                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4691                "## Common information\n\n",
4692                "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]),
4693                "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]),
4694                "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]),
4695                "* **Allowed to work with instruments:**\n{}\n".format("".join(["  - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])),
4696                "\n## User accounts\n\n",
4697            ]
4698
4699            for account in view["stat"]["accounts"].keys():
4700                info.extend([
4701                    "### ID: [{}]\n\n".format(account),
4702                    "| Parameters           | Values                                                       |\n",
4703                    "|----------------------|--------------------------------------------------------------|\n",
4704                    "| Account type:        | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]),
4705                    "| Account name:        | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]),
4706                    "| Account status:      | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]),
4707                    "| Access level:        | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]),
4708                    "| Date opened:         | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]),
4709                    "| Date closed:         | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]),
4710                ])
4711
4712                if margins[account]:
4713                    info.extend([
4714                        "| Margin status:       | Enabled                                                      |\n",
4715                        "| - Liquid portfolio:  | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])),
4716                        "| - Margin starting:   | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])),
4717                        "| - Margin minimum:    | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])),
4718                        "| - Margin difference: | {:<60} |\n".format("{} {}".format(margins[account]["diff"], margins[account]["currency"])),
4719                        "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)),
4720                        "| - Not covered funds: | {:<60} |\n\n".format("{:.2f} {}".format(margins[account]["missing"], margins[account]["currency"])),
4721                    ])
4722
4723                else:
4724                    info.append("| Margin status:       | Disabled                                                     |\n\n")
4725
4726            info.extend([
4727                "\n## Current user tariff limits\n",
4728                "\n### See also\n",
4729                "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n",
4730                "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n",
4731                "  - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n",
4732                "  - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n",
4733                "\n### Unary limits\n",
4734            ])
4735
4736            if unary:
4737                for key, values in sorted(unary.items()):
4738                    info.append("\n* Max requests per minute: {}\n".format(key))
4739
4740                    for value in values:
4741                        info.append("  - {}\n".format(value))
4742
4743            else:
4744                info.append("\nNot available\n")
4745
4746            info.append("\n### Stream limits\n")
4747
4748            if stream:
4749                for key, values in sorted(stream.items()):
4750                    info.append("\n* Max stream connections: {}\n".format(key))
4751
4752                    for value in values:
4753                        info.append("  - {}\n".format(value))
4754
4755            else:
4756                info.append("\nNot available\n")
4757
4758            infoText = "".join(info)
4759
4760            if show and not onlyFiles:
4761                uLogger.info(infoText)
4762
4763            if self.userInfoFile and (show or onlyFiles):
4764                with open(self.userInfoFile, "w", encoding="UTF-8") as fH:
4765                    fH.write(infoText)
4766
4767                uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile)))
4768
4769                if self.useHTMLReports:
4770                    htmlFilePath = self.userInfoFile.replace(".md", ".html") if self.userInfoFile.endswith(".md") else self.userInfoFile + ".html"
4771                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
4772                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User info", commonCSS=COMMON_CSS, markdown=infoText))
4773
4774                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
4775
4776        return view
4777
4778
4779class Args:
4780    """
4781    If `Main()` function is imported as module, then this class used to convert arguments from **kwargs as object.
4782    """
4783    def __init__(self, **kwargs):
4784        self.__dict__.update(kwargs)
4785
4786    def __getattr__(self, item):
4787        return None
4788
4789
4790def ParseArgs():
4791    """This function get and parse command line keys."""
4792    parser = ArgumentParser()  # command-line string parser
4793
4794    parser.description = "TKSBrokerAPI is a trading platform for automation on Python to simplify the implementation of trading scenarios and work with Tinkoff Invest API server via the REST protocol. See examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md"
4795    parser.usage = "\n/as module/ python TKSBrokerAPI.py [some options] [one command]\n/as CLI tool/ tksbrokerapi [some options] [one command]"
4796
4797    # --- options:
4798
4799    parser.add_argument("--no-cache", action="store_true", default=False, help="Option: not use local cache `dump.json`, but update raw instruments data when starting the platform. `False` by default.")
4800    parser.add_argument("--token", type=str, help="Option: Tinkoff service's api key. If not set then used environment variable `TKS_API_TOKEN`. See how to use: https://tinkoff.github.io/investAPI/token/")
4801    parser.add_argument("--account-id", type=str, default=None, help="Option: string with an user numeric account ID in Tinkoff Broker. It can be found in any broker's reports (see the contract number). Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.")
4802
4803    parser.add_argument("--ticker", "-t", type=str, help="Option: instrument's ticker, e.g. `IBM`, `YNDX`, `GOOGL` etc. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR`.")
4804    parser.add_argument("--figi", "-f", type=str, help="Option: instrument's FIGI, e.g. `BBG006L8G4H1` (for `YNDX`).")
4805
4806    parser.add_argument("--depth", type=int, default=1, help="Option: Depth of Market (DOM) can be >=1, 1 by default.")
4807    parser.add_argument("--no-cancelled", "--no-canceled", action="store_true", default=False, help="Option: remove information about cancelled operations from the deals report by the `--deals` key. `False` by default.")
4808
4809    parser.add_argument("--output", type=str, default=None, help="Option: replace default paths to output files for some commands. If `None` then used default files.")
4810    parser.add_argument("--html", "--HTML", action="store_true", default=False, help="Option: if key present then TKSBrokerAPI generate also HTML reports from Markdown. False by default.")
4811
4812    parser.add_argument("--interval", type=str, default="hour", help="Option: available values are `1min`, `5min`, `15min`, `hour` and `day`. Used only with `--history` key. This is time period of one candle. Default: `hour` for every history candles.")
4813    parser.add_argument("--only-missing", action="store_true", default=False, help="Option: if history file define by `--output` key then add only last missing candles, do not request all history length. `False` by default.")
4814    parser.add_argument("--csv-sep", type=str, default=",", help="Option: separator if csv-file is used, `,` by default.")
4815
4816    parser.add_argument("--debug-level", "--log-level", "--verbosity", "-v", type=int, default=20, help="Option: showing STDOUT messages of minimal debug level, e.g. 10 = DEBUG, 20 = INFO, 30 = WARNING, 40 = ERROR, 50 = CRITICAL. INFO (20) by default.")
4817    parser.add_argument("--more", "--more-debug", action="store_true", default=False, help="Option: `--debug-level` key only switch log level verbosity, but in addition `--more` key enable all debug information, such as net request and response headers in all methods.")
4818    parser.add_argument("--tag", type=str, default="", help="Option: identification TKSBrokerAPI tag in log messages to simplify debugging when platform instances runs in parallel mode. Default: `""` (empty string).")
4819
4820    # --- commands:
4821
4822    parser.add_argument("--version", "--ver", action="store_true", help="Action: shows current semantic version, looks like `major.minor.buildnumber`. If TKSBrokerAPI not installed via pip, then used local build number `.dev0`.")
4823
4824    parser.add_argument("--list", "-l", action="store_true", help="Action: get and print all available instruments and some information from broker server. Also, you can define `--output` key to save list of instruments to file, default: `instruments.md`.")
4825    parser.add_argument("--list-xlsx", "-x", action="store_true", help="Action: get all available instruments from server for current account and save raw data into xlsx-file for further used by data scientists or stock analytics, default: `dump.xlsx`.")
4826    parser.add_argument("--bonds-xlsx", "-b", type=str, nargs="*", help="Action: get all available bonds if only key present or list of bonds with FIGIs or tickers and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bonds payment calendar, coupon yields, current yields and some statistics etc. And then export data to XLSX-file, default: `ext-bonds.xlsx` or you can change it with `--output` key. WARNING! This is too long operation if a lot of bonds requested from broker server.")
4827    parser.add_argument("--search", "-s", type=str, nargs=1, help="Action: search for an instruments by part of the name, ticker or FIGI. Also, you can define `--output` key to save results to file, default: `search-results.md`.")
4828    parser.add_argument("--info", "-i", action="store_true", help="Action: get information from broker server about instrument by it's ticker or FIGI. `--ticker` key or `--figi` key must be defined!")
4829    parser.add_argument("--calendar", "-c", type=str, nargs="*", help="Action: show bonds payment calendar as a table. Calendar build for one or more tickers or FIGIs, or for all bonds if only key present. If the `--output` key present then calendar saves to file, default: `calendar.md`. Also, created XLSX-file with bond payments calendar for further used by data scientists or stock analytics, `calendar.xlsx` by default. WARNING! This is too long operation if a lot of bonds requested from broker server.")
4830    parser.add_argument("--price", action="store_true", help="Action: show actual price list for current instrument. Also, you can use `--depth` key. `--ticker` key or `--figi` key must be defined!")
4831    parser.add_argument("--prices", "-p", type=str, nargs="+", help="Action: get and print current prices for list of given instruments (by it's tickers or by FIGIs). WARNING! This is too long operation if you request a lot of instruments! Also, you can define `--output` key to save list of prices to file, default: `prices.md`.")
4832
4833    parser.add_argument("--overview", "-o", action="store_true", help="Action: shows all open positions, orders and some statistics. Also, you can define `--output` key to save this information to file, default: `overview.md`.")
4834    parser.add_argument("--overview-digest", action="store_true", help="Action: shows a short digest of the portfolio status. Also, you can define `--output` key to save this information to file, default: `overview-digest.md`.")
4835    parser.add_argument("--overview-positions", action="store_true", help="Action: shows only open positions. Also, you can define `--output` key to save this information to file, default: `overview-positions.md`.")
4836    parser.add_argument("--overview-orders", action="store_true", help="Action: shows only sections of open limits and stop orders. Also, you can define `--output` key to save orders to file, default: `overview-orders.md`.")
4837    parser.add_argument("--overview-analytics", action="store_true", help="Action: shows only the analytics section and the distribution of the portfolio by various categories. Also, you can define `--output` key to save this information to file, default: `overview-analytics.md`.")
4838    parser.add_argument("--overview-calendar", action="store_true", help="Action: shows only the bonds calendar section (if these present in portfolio). Also, you can define `--output` key to save this information to file, default: `overview-calendar.md`.")
4839
4840    parser.add_argument("--deals", "-d", type=str, nargs="*", help="Action: show all deals between two given dates. Start day may be an integer number: -1, -2, -3 days ago. Also, you can use keywords: `today`, `yesterday` (-1), `week` (-7), `month` (-30) and `year` (-365). Dates format must be: `%%Y-%%m-%%d`, e.g. 2020-02-03. With `--no-cancelled` key information about cancelled operations will be removed from the deals report. Also, you can define `--output` key to save all deals to file, default: `deals.md`.")
4841    parser.add_argument("--history", type=str, nargs="*", help="Action: get last history candles of the current instrument defined by `--ticker` or `--figi` (FIGI id) keys. History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. This action may be used together with the `--render-chart` key. Also, you can define `--output` key to save history candlesticks to file.")
4842    parser.add_argument("--load-history", type=str, help="Action: try to load history candles from given csv-file as a Pandas Dataframe and print it in to the console. This action may be used together with the `--render-chart` key.")
4843    parser.add_argument("--render-chart", type=str, help="Action: render candlesticks chart. This key may only used with `--history` or `--load-history` together. Action has 1 parameter with two possible string values: `interact` (`i`) or `non-interact` (`ni`).")
4844
4845    parser.add_argument("--trade", nargs="*", help="Action: universal action to open market position for defined ticker or FIGI. You must specify 1-5 parameters: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. See examples in readme.")
4846    parser.add_argument("--buy", nargs="*", help="Action: immediately open BUY market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].")
4847    parser.add_argument("--sell", nargs="*", help="Action: immediately open SELL market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].")
4848
4849    parser.add_argument("--order", nargs="*", help="Action: universal action to open limit or stop-order in any directions. You must specify 4-7 parameters: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]]. See examples in readme.")
4850    parser.add_argument("--buy-limit", type=float, nargs=2, help="Action: open pending BUY limit-order (below current price). You must specify only 2 parameters: [lots] [target price] to open BUY limit-order. If you try to create `Buy` limit-order above current price then broker immediately open `Buy` market order, such as if you do simple `--buy` operation!")
4851    parser.add_argument("--sell-limit", type=float, nargs=2, help="Action: open pending SELL limit-order (above current price). You must specify only 2 parameters: [lots] [target price] to open SELL limit-order. If you try to create `Sell` limit-order below current price then broker immediately open `Sell` market order, such as if you do simple `--sell` operation!")
4852    parser.add_argument("--buy-stop", nargs="*", help="Action: open BUY stop-order. You must specify at least 2 parameters: [lots] [target price] to open BUY stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.")
4853    parser.add_argument("--sell-stop", nargs="*", help="Action: open SELL stop-order. You must specify at least 2 parameters: [lots] [target price] to open SELL stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.")
4854    # parser.add_argument("--buy-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending BUY limit-orders (below current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!")
4855    # parser.add_argument("--sell-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending SELL limit-orders (above current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!")
4856
4857    parser.add_argument("--close-order", "--cancel-order", type=str, nargs=1, help="Action: close only one order by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.")
4858    parser.add_argument("--close-orders", "--cancel-orders", type=str, nargs="+", help="Action: close one or list of orders by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.")
4859    parser.add_argument("--close-trade", "--cancel-trade", action="store_true", help="Action: close only one position for instrument defined by `--ticker` (high priority) or `--figi` keys, including for currencies tickers.")
4860    parser.add_argument("--close-trades", "--cancel-trades", type=str, nargs="+", help="Action: close positions for list of tickers or FIGIs, including for currencies tickers or FIGIs.")
4861    parser.add_argument("--close-all", "--cancel-all", type=str, nargs="*", help="Action: close all available (not blocked) opened trades and orders, excluding for currencies. Also you can select one or more keywords case insensitive to specify trades type: `orders`, `shares`, `bonds`, `etfs` and `futures`, but not `currencies`. Currency positions you must closes manually using `--buy`, `--sell`, `--close-trade` or `--close-trades` operations. If the `--close-all` key present with the `--ticker` or `--figi` keys, then positions and all open limit and stop orders for the specified instrument are closed.")
4862
4863    parser.add_argument("--limits", "--withdrawal-limits", "-w", action="store_true", help="Action: show table of funds available for withdrawal for current `accountId`. You can change `accountId` with the key `--account-id`. Also, you can define `--output` key to save this information to file, default: `limits.md`.")
4864    parser.add_argument("--user-info", "-u", action="store_true", help="Action: show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). Also, you can define `--output` key to save this information to file, default: `user-info.md`.")
4865    parser.add_argument("--account", "--accounts", "-a", action="store_true", help="Action: show simple table with all available user accounts. Also, you can define `--output` key to save this information to file, default: `accounts.md`.")
4866
4867    cmdArgs = parser.parse_args()
4868    return cmdArgs
4869
4870
4871def Main(**kwargs):
4872    """
4873    Main function for work with TKSBrokerAPI in the console.
4874
4875    See examples:
4876    - in english: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md
4877    - in russian: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README.md
4878    """
4879    args = Args(**kwargs) if kwargs else ParseArgs()  # get and parse command-line parameters or use **kwarg parameters
4880
4881    if args.debug_level:
4882        uLogger.level = 10  # always debug level by default
4883        uLogger.handlers[0].level = args.debug_level  # level for STDOUT
4884
4885    exitCode = 0
4886    start = datetime.now(tzutc())
4887    uLogger.debug("=-" * 50)
4888    uLogger.debug(">>> TKSBrokerAPI module started at: [{}] UTC, it is [{}] local time".format(
4889        start.strftime(TKS_PRINT_DATE_TIME_FORMAT),
4890        start.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4891    ))
4892
4893    # trying to calculate full current version:
4894    buildVersion = __version__
4895    try:
4896        v = version("tksbrokerapi")
4897        buildVersion = v if v.startswith(buildVersion) else buildVersion + ".dev0"  # set version as major.minor.dev0 if run as local build or local script
4898
4899    except Exception:
4900        buildVersion = __version__ + ".dev0"  # if an errors occurred then also set version as major.minor.dev0
4901
4902    uLogger.debug("TKSBrokerAPI major.minor.build version used: [{}]".format(buildVersion))
4903    uLogger.debug("Host CPU count: [{}]".format(CPU_COUNT))
4904
4905    try:
4906        if args.version:
4907            print("TKSBrokerAPI {}".format(buildVersion))
4908            uLogger.debug("User requested current TKSBrokerAPI major.minor.build version: [{}]".format(buildVersion))
4909
4910        else:
4911            # Init class for trading with Tinkoff Broker:
4912            trader = TinkoffBrokerServer(
4913                token=args.token,
4914                accountId=args.account_id,
4915                useCache=not args.no_cache,
4916            )
4917
4918            if args.tag is not None:
4919                trader.tag = args.tag  # Identification TKSBrokerAPI tag in log messages to simplify debugging when platform instances runs in parallel mode
4920
4921            # --- set some options:
4922
4923            if args.more:
4924                trader.moreDebug = True
4925                uLogger.warning("More debug info mode is enabled! See network requests, responses and its headers in the full log or run TKSBrokerAPI platform with the `--verbosity 10` to show theres in console.")
4926
4927            if args.html:
4928                trader.useHTMLReports = True
4929
4930            if args.ticker:
4931                ticker = str(args.ticker).upper()  # Tickers may be upper case only
4932
4933                if ticker in trader.aliasesKeys:
4934                    trader.ticker = trader.aliases[ticker]  # Replace some tickers with its aliases
4935
4936                else:
4937                    trader.ticker = ticker
4938
4939            if args.figi:
4940                trader.figi = str(args.figi).upper()  # FIGIs may be upper case only
4941
4942            if args.depth is not None:
4943                trader.depth = args.depth
4944
4945            # --- do one command:
4946
4947            if args.list:
4948                if args.output is not None:
4949                    trader.instrumentsFile = args.output
4950
4951                trader.ShowInstrumentsInfo(show=True)
4952
4953            elif args.list_xlsx:
4954                trader.DumpInstrumentsAsXLSX(forceUpdate=False)
4955
4956            elif args.bonds_xlsx is not None:
4957                if args.output is not None:
4958                    trader.bondsXLSXFile = args.output
4959
4960                if len(args.bonds_xlsx) == 0:
4961                    trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=True)  # request bonds with all available tickers
4962
4963                else:
4964                    trader.ExtendBondsData(instruments=args.bonds_xlsx, xlsx=True)  # request list of given bonds
4965
4966            elif args.search:
4967                if args.output is not None:
4968                    trader.searchResultsFile = args.output
4969
4970                trader.SearchInstruments(pattern=args.search[0], show=True)
4971
4972            elif args.info:
4973                if not (args.ticker or args.figi):
4974                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
4975                    raise Exception("Ticker or FIGI required")
4976
4977                if args.output is not None:
4978                    trader.infoFile = args.output
4979
4980                if args.ticker:
4981                    trader.SearchByTicker(requestPrice=True, show=True)  # show info and current prices by ticker name
4982
4983                else:
4984                    trader.SearchByFIGI(requestPrice=True, show=True)  # show info and current prices by FIGI id
4985
4986            elif args.calendar is not None:
4987                if args.output is not None:
4988                    trader.calendarFile = args.output
4989
4990                if len(args.calendar) == 0:
4991                    bondsData = trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=False)  # request bonds with all available tickers
4992
4993                else:
4994                    bondsData = trader.ExtendBondsData(instruments=args.calendar, xlsx=False)  # request list of given bonds
4995
4996                trader.ShowBondsCalendar(extBonds=bondsData, show=True)  # shows bonds payment calendar only
4997
4998            elif args.price:
4999                if not (args.ticker or args.figi):
5000                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
5001                    raise Exception("Ticker or FIGI required")
5002
5003                trader.GetCurrentPrices(show=True)
5004
5005            elif args.prices is not None:
5006                if args.output is not None:
5007                    trader.pricesFile = args.output
5008
5009                trader.GetListOfPrices(instruments=args.prices, show=True)  # WARNING: too long wait for a lot of instruments prices
5010
5011            elif args.overview:
5012                if args.output is not None:
5013                    trader.overviewFile = args.output
5014
5015                trader.Overview(show=True, details="full")
5016
5017            elif args.overview_digest:
5018                if args.output is not None:
5019                    trader.overviewDigestFile = args.output
5020
5021                trader.Overview(show=True, details="digest")
5022
5023            elif args.overview_positions:
5024                if args.output is not None:
5025                    trader.overviewPositionsFile = args.output
5026
5027                trader.Overview(show=True, details="positions")
5028
5029            elif args.overview_orders:
5030                if args.output is not None:
5031                    trader.overviewOrdersFile = args.output
5032
5033                trader.Overview(show=True, details="orders")
5034
5035            elif args.overview_analytics:
5036                if args.output is not None:
5037                    trader.overviewAnalyticsFile = args.output
5038
5039                trader.Overview(show=True, details="analytics")
5040
5041            elif args.overview_calendar:
5042                if args.output is not None:
5043                    trader.overviewAnalyticsFile = args.output
5044
5045                trader.Overview(show=True, details="calendar")
5046
5047            elif args.deals is not None:
5048                if args.output is not None:
5049                    trader.reportFile = args.output
5050
5051                if 0 <= len(args.deals) < 3:
5052                    trader.Deals(
5053                        start=args.deals[0] if len(args.deals) >= 1 else None,
5054                        end=args.deals[1] if len(args.deals) == 2 else None,
5055                        show=True,  # Always show deals report in console
5056                        showCancelled=not args.no_cancelled,  # If --no-cancelled key then remove cancelled operations from the deals report. False by default.
5057                    )
5058
5059                else:
5060                    uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]")
5061                    raise Exception("Incorrect value")
5062
5063            elif args.history is not None:
5064                if args.output is not None:
5065                    trader.historyFile = args.output
5066
5067                if 0 <= len(args.history) < 3:
5068                    dataReceived = trader.History(
5069                        start=args.history[0] if len(args.history) >= 1 else None,
5070                        end=args.history[1] if len(args.history) == 2 else None,
5071                        interval="hour" if args.interval is None or not args.interval else args.interval,
5072                        onlyMissing=False if args.only_missing is None or not args.only_missing else args.only_missing,
5073                        csvSep="," if args.csv_sep is None or not args.csv_sep else args.csv_sep,
5074                        show=True,  # shows all downloaded candles in console
5075                    )
5076
5077                    if args.render_chart is not None and dataReceived is not None:
5078                        iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True
5079
5080                        trader.ShowHistoryChart(
5081                            candles=dataReceived,
5082                            interact=iChart,
5083                            openInBrowser=False,  # False by default, to avoid issues with `permissions denied` to html-file.
5084                        )
5085
5086                else:
5087                    uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]")
5088                    raise Exception("Incorrect value")
5089
5090            elif args.load_history is not None:
5091                histData = trader.LoadHistory(filePath=args.load_history)  # load data from file and show history in console
5092
5093                if args.render_chart is not None and histData is not None:
5094                    iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True
5095                    trader.ticker = os.path.basename(args.load_history)  # use filename as ticker name for PriceGenerator's chart
5096
5097                    trader.ShowHistoryChart(
5098                        candles=histData,
5099                        interact=iChart,
5100                        openInBrowser=False,  # False by default, to avoid issues with `permissions denied` to html-file.
5101                    )
5102
5103            elif args.trade is not None:
5104                if 1 <= len(args.trade) <= 5:
5105                    trader.Trade(
5106                        operation=args.trade[0],
5107                        lots=int(args.trade[1]) if len(args.trade) >= 2 else 1,
5108                        tp=float(args.trade[2]) if len(args.trade) >= 3 else 0.,
5109                        sl=float(args.trade[3]) if len(args.trade) >= 4 else 0.,
5110                        expDate=args.trade[4] if len(args.trade) == 5 else "Undefined",
5111                    )
5112
5113                else:
5114                    uLogger.error("You must specify 1-5 parameters to open trade: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
5115
5116            elif args.buy is not None:
5117                if 0 <= len(args.buy) <= 4:
5118                    trader.Buy(
5119                        lots=int(args.buy[0]) if len(args.buy) >= 1 else 1,
5120                        tp=float(args.buy[1]) if len(args.buy) >= 2 else 0.,
5121                        sl=float(args.buy[2]) if len(args.buy) >= 3 else 0.,
5122                        expDate=args.buy[3] if len(args.buy) == 4 else "Undefined",
5123                    )
5124
5125                else:
5126                    uLogger.error("You must specify 0-4 parameters to open buy position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
5127
5128            elif args.sell is not None:
5129                if 0 <= len(args.sell) <= 4:
5130                    trader.Sell(
5131                        lots=int(args.sell[0]) if len(args.sell) >= 1 else 1,
5132                        tp=float(args.sell[1]) if len(args.sell) >= 2 else 0.,
5133                        sl=float(args.sell[2]) if len(args.sell) >= 3 else 0.,
5134                        expDate=args.sell[3] if len(args.sell) == 4 else "Undefined",
5135                    )
5136
5137                else:
5138                    uLogger.error("You must specify 0-4 parameters to open sell position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
5139
5140            elif args.order:
5141                if 4 <= len(args.order) <= 7:
5142                    trader.Order(
5143                        operation=args.order[0],
5144                        orderType=args.order[1],
5145                        lots=int(args.order[2]),
5146                        targetPrice=float(args.order[3]),
5147                        limitPrice=float(args.order[4]) if len(args.order) >= 5 else 0.,
5148                        stopType=args.order[5] if len(args.order) >= 6 else "Limit",
5149                        expDate=args.order[6] if len(args.order) == 7 else "Undefined",
5150                    )
5151
5152                else:
5153                    uLogger.error("You must specify 4-7 parameters to open order: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]]. See: `python TKSBrokerAPI.py --help`")
5154
5155            elif args.buy_limit:
5156                trader.BuyLimit(lots=int(args.buy_limit[0]), targetPrice=args.buy_limit[1])
5157
5158            elif args.sell_limit:
5159                trader.SellLimit(lots=int(args.sell_limit[0]), targetPrice=args.sell_limit[1])
5160
5161            elif args.buy_stop:
5162                if 2 <= len(args.buy_stop) <= 7:
5163                    trader.BuyStop(
5164                        lots=int(args.buy_stop[0]),
5165                        targetPrice=float(args.buy_stop[1]),
5166                        limitPrice=float(args.buy_stop[2]) if len(args.buy_stop) >= 3 else 0.,
5167                        stopType=args.buy_stop[3] if len(args.buy_stop) >= 4 else "Limit",
5168                        expDate=args.buy_stop[4] if len(args.buy_stop) == 5 else "Undefined",
5169                    )
5170
5171                else:
5172                    uLogger.error("You must specify 2-5 parameters for buy stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
5173
5174            elif args.sell_stop:
5175                if 2 <= len(args.sell_stop) <= 7:
5176                    trader.SellStop(
5177                        lots=int(args.sell_stop[0]),
5178                        targetPrice=float(args.sell_stop[1]),
5179                        limitPrice=float(args.sell_stop[2]) if len(args.sell_stop) >= 3 else 0.,
5180                        stopType=args.sell_stop[3] if len(args.sell_stop) >= 4 else "Limit",
5181                        expDate=args.sell_stop[4] if len(args.sell_stop) == 5 else "Undefined",
5182                    )
5183
5184                else:
5185                    uLogger.error("You must specify 2-5 parameters for sell stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: python TKSBrokerAPI.py --help")
5186
5187            # elif args.buy_order_grid is not None:
5188            #     # update order grid work with api v2
5189            #     if len(args.buy_order_grid) == 2:
5190            #         orderParams = trader.ParseOrderParameters(operation="Buy", **dict(kw.split('=') for kw in args.buy_order_grid))
5191            #
5192            #         for order in orderParams:
5193            #             trader.Order(operation="Buy", lots=order["lot"], price=order["price"])
5194            #
5195            #     else:
5196            #         uLogger.error("To open grid of pending BUY limit-orders (below current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`")
5197            #
5198            # elif args.sell_order_grid is not None:
5199            #     # update order grid work with api v2
5200            #     if len(args.sell_order_grid) >= 2:
5201            #         orderParams = trader.ParseOrderParameters(operation="Sell", **dict(kw.split('=') for kw in args.sell_order_grid))
5202            #
5203            #         for order in orderParams:
5204            #             trader.Order(operation="Sell", lots=order["lot"], price=order["price"])
5205            #
5206            #     else:
5207            #         uLogger.error("To open grid of pending SELL limit-orders (above current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`")
5208
5209            elif args.close_order is not None:
5210                trader.CloseOrders(args.close_order)  # close only one order
5211
5212            elif args.close_orders is not None:
5213                trader.CloseOrders(args.close_orders)  # close list of orders
5214
5215            elif args.close_trade:
5216                if not (args.ticker or args.figi):
5217                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
5218                    raise Exception("Ticker or FIGI required")
5219
5220                if args.ticker:
5221                    trader.CloseTrades([str(args.ticker).upper()])  # close only one trade by ticker (priority)
5222
5223                else:
5224                    trader.CloseTrades([str(args.figi).upper()])  # close only one trade by FIGI
5225
5226            elif args.close_trades is not None:
5227                trader.CloseTrades(args.close_trades)  # close trades for list of tickers
5228
5229            elif args.close_all is not None:
5230                if args.ticker:
5231                    trader.CloseAllByTicker(instrument=str(args.ticker).upper())
5232
5233                elif args.figi:
5234                    trader.CloseAllByFIGI(instrument=str(args.figi).upper())
5235
5236                else:
5237                    trader.CloseAll(*args.close_all)
5238
5239            elif args.limits:
5240                if args.output is not None:
5241                    trader.withdrawalLimitsFile = args.output
5242
5243                trader.OverviewLimits(show=True)
5244
5245            elif args.user_info:
5246                if args.output is not None:
5247                    trader.userInfoFile = args.output
5248
5249                trader.OverviewUserInfo(show=True)
5250
5251            elif args.account:
5252                if args.output is not None:
5253                    trader.userAccountsFile = args.output
5254
5255                trader.OverviewAccounts(show=True)
5256
5257            else:
5258                uLogger.error("There is no command to execute! One of the possible commands must be selected. See help with `--help` key.")
5259                raise Exception("There is no command to execute")
5260
5261    except Exception:
5262        trace = tb.format_exc()
5263        for e in ["socket.gaierror", "nodename nor servname provided", "or not known", "NewConnectionError", "[Errno 8]", "Failed to establish a new connection"]:
5264            if e in trace:
5265                uLogger.error("Check your Internet connection! Failed to establish connection to broker server!")
5266                break
5267
5268        uLogger.debug(trace)
5269        uLogger.debug("Please, check issues or request a new one at https://github.com/Tim55667757/TKSBrokerAPI/issues")
5270        exitCode = 255  # an error occurred, must be open a ticket for this issue
5271
5272    finally:
5273        finish = datetime.now(tzutc())
5274
5275        if exitCode == 0:
5276            if args.more:
5277                uLogger.debug("All operations were finished success (summary code is 0).")
5278
5279        else:
5280            uLogger.error("An issue occurred with TKSBrokerAPI module! See full debug log in [{}] or run TKSBrokerAPI once again with the key `--debug-level 10`. Summary code: {}".format(
5281                os.path.abspath(uLog.defaultLogFile), exitCode,
5282            ))
5283
5284        uLogger.debug(">>> TKSBrokerAPI module work duration: [{}]".format(finish - start))
5285        uLogger.debug(">>> TKSBrokerAPI module finished: [{} UTC], it is [{}] local time".format(
5286            finish.strftime(TKS_PRINT_DATE_TIME_FORMAT),
5287            finish.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
5288        ))
5289        uLogger.debug("=-" * 50)
5290
5291        if not kwargs:
5292            sys.exit(exitCode)
5293
5294        else:
5295            return exitCode
5296
5297
5298if __name__ == "__main__":
5299    Main()
class TinkoffBrokerServer:
  78class TinkoffBrokerServer:
  79    """
  80    This class implements methods to work with Tinkoff broker server.
  81
  82    Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/
  83
  84    About `token`: https://tinkoff.github.io/investAPI/token/
  85    """
  86    def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None:
  87        """
  88        Main class init.
  89
  90        :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`.
  91        :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports.
  92                          Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.
  93        :param useCache: use default cache file with raw data to use instead of `iList`.
  94                         True by default. Cache is auto-update if new day has come.
  95                         If you don't want to use cache and always updates raw data then set `useCache=False`.
  96        :param defaultCache: path to default cache file. `dump.json` by default.
  97        """
  98        if token is None or not token:
  99            try:
 100                self.token = r"{}".format(os.environ["TKS_API_TOKEN"])
 101                uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/")
 102
 103            except KeyError:
 104                uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/")
 105                raise Exception("Token required")
 106
 107        else:
 108            self.token = token  # highly priority than environment variable 'TKS_API_TOKEN'
 109            uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`")
 110
 111        if accountId is None or not accountId:
 112            try:
 113                self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"])
 114                uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId))
 115
 116            except KeyError:
 117                uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).")
 118
 119        else:
 120            self.accountId = accountId  # highly priority than environment variable 'TKS_ACCOUNT_ID'
 121            uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId))
 122
 123        self.version = __version__  # duplicate here used TKSBrokerAPI main version
 124        """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only.
 125
 126        Latest version: https://pypi.org/project/tksbrokerapi/
 127        """
 128
 129        self._tag = ""
 130        """Identification TKSBrokerAPI tag in log messages to simplify debugging when platform instances runs in parallel mode. Default: `""` (empty string)."""
 131
 132        self.__lock = Lock()  # initialize multiprocessing mutex lock
 133
 134        self.aliases = TKS_TICKER_ALIASES
 135        """Some aliases instead official tickers.
 136
 137        See also: `TKSEnums.TKS_TICKER_ALIASES`
 138        """
 139
 140        self.aliasesKeys = self.aliases.keys()  # re-calc only first time at class init
 141
 142        self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED  # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there
 143
 144        self._ticker = ""
 145        """String with ticker, e.g. `GOOGL`. Tickers may be upper case only.
 146
 147        Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc.
 148        More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`.
 149
 150        See also: `SearchByTicker()`, `SearchInstruments()`.
 151        """
 152
 153        self._figi = ""
 154        """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only.
 155
 156        See also: `SearchByFIGI()`, `SearchInstruments()`.
 157        """
 158
 159        self.depth = 1
 160        """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI.
 161
 162        See also: `GetCurrentPrices()`.
 163        """
 164
 165        self.server = r"https://invest-public-api.tinkoff.ru/rest"
 166        """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest
 167
 168        See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`.
 169        """
 170
 171        uLogger.debug("Broker API server: {}".format(self.server))
 172
 173        self.timeout = 15
 174        """Server operations timeout in seconds. Default: `15`.
 175
 176        See also: `SendAPIRequest()`.
 177        """
 178
 179        self.headers = {
 180            "Content-Type": "application/json",
 181            "accept": "application/json",
 182            "Authorization": "Bearer {}".format(self.token),
 183            "x-app-name": "Tim55667757.TKSBrokerAPI",
 184        }
 185        """
 186        Headers which send in every request to broker server. Please, do not change it!
 187        Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}", "x-app-name": "Tim55667757.TKSBrokerAPI"}`.
 188
 189        See also: `SendAPIRequest()`.
 190        """
 191
 192        self.body = None
 193        """Request body which send to broker server. Default: `None`.
 194
 195        See also: `SendAPIRequest()`.
 196        """
 197
 198        self.moreDebug = False
 199        """Enables more debug information in this class, such as net request and response headers in all methods. `False` by default."""
 200
 201        self.useHTMLReports = False
 202        """
 203        If `True` then TKSBrokerAPI generate also HTML reports from Markdown. `False` by default.
 204        
 205        See also: Mako Templates for Python (https://www.makotemplates.org/). Mako is a template library provides simple syntax and maximum performance.
 206        """
 207
 208        self.historyFile = None
 209        """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame.
 210
 211        See also: `History()`.
 212        """
 213
 214        self.htmlHistoryFile = "index.html"
 215        """Full path to the html file where rendered candles chart stored. Default: `index.html`.
 216
 217        See also: `ShowHistoryChart()`.
 218        """
 219
 220        self.instrumentsFile = "instruments.md"
 221        """Filename where full available to user instruments list will be saved. Default: `instruments.md`.
 222
 223        See also: `ShowInstrumentsInfo()`.
 224        """
 225
 226        self.searchResultsFile = "search-results.md"
 227        """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`.
 228
 229        See also: `SearchInstruments()`.
 230        """
 231
 232        self.pricesFile = "prices.md"
 233        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
 234
 235        See also: `GetListOfPrices()`.
 236        """
 237
 238        self.infoFile = "info.md"
 239        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
 240
 241        See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`.
 242        """
 243
 244        self.bondsXLSXFile = "ext-bonds.xlsx"
 245        """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 
 246        bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`.
 247
 248        See also: `ExtendBondsData()`.
 249        """
 250
 251        self.calendarFile = "calendar.md"
 252        """Filename where bonds payment calendar will be saved. Default: `calendar.md`.
 253        
 254        Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`.
 255
 256        See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`.
 257        """
 258
 259        self.overviewFile = "overview.md"
 260        """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`.
 261
 262        See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`.
 263        """
 264
 265        self.overviewDigestFile = "overview-digest.md"
 266        """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`.
 267
 268        See also: `Overview()` with parameter `details="digest"`.
 269        """
 270
 271        self.overviewPositionsFile = "overview-positions.md"
 272        """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`.
 273
 274        See also: `Overview()` with parameter `details="positions"`.
 275        """
 276
 277        self.overviewOrdersFile = "overview-orders.md"
 278        """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`.
 279
 280        See also: `Overview()` with parameter `details="orders"`.
 281        """
 282
 283        self.overviewAnalyticsFile = "overview-analytics.md"
 284        """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`.
 285
 286        See also: `Overview()` with parameter `details="analytics"`.
 287        """
 288
 289        self.overviewBondsCalendarFile = "overview-calendar.md"
 290        """Filename where only the bonds calendar section will be saved. Default: `overview-calendar.md`.
 291
 292        See also: `Overview()` with parameter `details="calendar"`.
 293        """
 294
 295        self.reportFile = "deals.md"
 296        """Filename where history of deals and trade statistics will be saved. Default: `deals.md`.
 297
 298        See also: `Deals()`.
 299        """
 300
 301        self.withdrawalLimitsFile = "limits.md"
 302        """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`.
 303
 304        See also: `OverviewLimits()` and `RequestLimits()`.
 305        """
 306
 307        self.userInfoFile = "user-info.md"
 308        """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`.
 309
 310        See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`.
 311        """
 312
 313        self.userAccountsFile = "accounts.md"
 314        """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`.
 315
 316        See also: `OverviewAccounts()`, `RequestAccounts()`.
 317        """
 318
 319        self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache
 320        """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`.
 321
 322        Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`.
 323
 324        See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`.
 325        """
 326
 327        self.iList = None  # init iList for raw instruments data
 328        """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`.
 329        
 330        See also: `Listing()`, `DumpInstruments()`.
 331        """
 332
 333        # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server:
 334        if useCache:
 335            if os.path.exists(self.iListDumpFile):
 336                dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc())  # dump modification date and time
 337                curTime = datetime.now(tzutc())
 338
 339                if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year):
 340                    uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
 341
 342                    self.DumpInstruments(forceUpdate=True)  # updating self.iList and dump file
 343
 344                else:
 345                    self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8"))  # load iList from dump
 346
 347                    uLogger.debug("Local cache with raw instruments data is used: [{}]. Last modified: [{}] UTC".format(
 348                        os.path.abspath(self.iListDumpFile),
 349                        dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT),
 350                    ))
 351
 352            else:
 353                uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...")
 354                self.DumpInstruments(forceUpdate=True)  # updating self.iList and creating default dump file
 355
 356        else:
 357            self.iList = self.Listing()  # request new raw instruments data from broker server
 358            self.DumpInstruments(forceUpdate=False)  # save raw instrument's data to default dump file `iListDumpFile`
 359
 360        self.priceModel = PriceGenerator()  # init PriceGenerator object to work with candles data
 361        """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on.
 362
 363        See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator
 364        """
 365
 366    @property
 367    def tag(self) -> str:
 368        """Identification TKSBrokerAPI tag in log messages to simplify debugging when platform instances runs in parallel mode. Default: `""` (empty string)."""
 369        return self._tag
 370
 371    @tag.setter
 372    def tag(self, value):
 373        """Setter for Identification TKSBrokerAPI tag in log messages to simplify debugging when platform instances runs in parallel mode. Default: `""` (empty string)."""
 374        self._tag = str(value)
 375
 376        if self._tag:
 377            for handler in uLogger.handlers:
 378                handler.setFormatter(uLog.logging.Formatter(uLog.formatStringWithTag.format(tag=self._tag)))
 379
 380            uLogger.debug("Custom TKSBrokerAPI tag was set: {}".format(self._tag))
 381
 382        else:
 383            for handler in uLogger.handlers:
 384                handler.setFormatter(uLog.logging.Formatter(uLog.formatString))
 385
 386            uLogger.debug("Default logger format is used")
 387
 388    @property
 389    def ticker(self) -> str:
 390        """String with ticker, e.g. `GOOGL`. Tickers may be upper case only.
 391
 392        Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc.
 393        More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`.
 394
 395        See also: `SearchByTicker()`, `SearchInstruments()`.
 396        """
 397        return self._ticker
 398
 399    @ticker.setter
 400    def ticker(self, value):
 401        """Setter for string with ticker, e.g. `GOOGL`. Tickers may be upper case only.
 402
 403        Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc.
 404        More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`.
 405
 406        See also: `SearchByTicker()`, `SearchInstruments()`.
 407        """
 408        self._ticker = str(value).upper()  # Tickers may be upper case only
 409
 410    @property
 411    def figi(self) -> str:
 412        """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only.
 413
 414        See also: `SearchByFIGI()`, `SearchInstruments()`.
 415        """
 416        return self._figi
 417
 418    @figi.setter
 419    def figi(self, value):
 420        """Setter for string with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only.
 421
 422        See also: `SearchByFIGI()`, `SearchInstruments()`.
 423        """
 424        self._figi = str(value).upper()  # FIGI may be upper case only
 425
 426    def _ParseJSON(self, rawData="{}") -> dict:
 427        """
 428        Parse JSON from response string.
 429
 430        :param rawData: this is a string with JSON-formatted text.
 431        :return: JSON (dictionary), parsed from server response string. If an error occurred, then returns empty dict `{}`.
 432        """
 433        try:
 434            responseJSON = json.loads(rawData) if rawData else {}
 435
 436            if self.moreDebug:
 437                uLogger.debug("JSON formatted raw body data of response:\n{}".format(json.dumps(responseJSON, indent=4)))
 438
 439            return responseJSON
 440
 441        except Exception as e:
 442            uLogger.error("An empty dict will be return, because an error occurred in `_ParseJSON()` method with comment: {}".format(e))
 443
 444            return {}
 445
 446    def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5) -> dict:
 447        """
 448        Send GET or POST request to broker server and receive JSON object.
 449
 450        self.header: must be defining with dictionary of headers.
 451        self.body: if define then used as request body. None by default.
 452        self.timeout: global request timeout, 15 seconds by default.
 453        :param url: url with REST request.
 454        :param reqType: send "GET" or "POST" request. "GET" by default.
 455        :param retry: how many times retry after first request if an 5xx server errors occurred.
 456        :param pause: sleep time in seconds between retries.
 457        :return: response JSON (dictionary) from broker.
 458        """
 459        if reqType.upper() not in ("GET", "POST"):
 460            uLogger.error("You can define request type: `GET` or `POST`!")
 461            raise Exception("Incorrect value")
 462
 463        if self.moreDebug:
 464            uLogger.debug("Request parameters:")
 465            uLogger.debug("    - REST API URL: {}".format(url))
 466            uLogger.debug("    - request type: {}".format(reqType))
 467            uLogger.debug("    - headers:\n{}".format(str(self.headers).replace(self.token, "*** request token ***")))
 468            uLogger.debug("    - body:\n{}".format(self.body))
 469
 470        # fast hack to avoid all operations with some tickers/FIGI
 471        responseJSON = {}
 472        oK = True
 473        for item in self.exclude:
 474            if item in url:
 475                if self.moreDebug:
 476                    uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude)))
 477
 478                oK = False
 479                break
 480
 481        if oK:
 482            with self.__lock:  # acquire the mutex lock
 483                counter = 0
 484                response = None
 485                errMsg = ""
 486
 487                while not response and counter <= retry:
 488                    if reqType == "GET":
 489                        response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout)
 490
 491                    if reqType == "POST":
 492                        response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout)
 493
 494                    if self.moreDebug:
 495                        uLogger.debug("Response:")
 496                        uLogger.debug("    - status code: {}".format(response.status_code))
 497                        uLogger.debug("    - reason: {}".format(response.reason))
 498                        uLogger.debug("    - body length: {}".format(len(response.text)))
 499                        uLogger.debug("    - headers:\n{}".format(response.headers))
 500
 501                    # Server returns some headers:
 502                    # - `x-ratelimit-limit` — shows the settings of the current user limit for this method.
 503                    # - `x-ratelimit-remaining` — the number of remaining requests of this type per minute.
 504                    # - `x-ratelimit-reset` — time in seconds before resetting the request counter.
 505                    # See: https://tinkoff.github.io/investAPI/grpc/#kreya
 506                    if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0":
 507                        rateLimitWait = int(response.headers["x-ratelimit-reset"])
 508                        uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait))
 509                        sleep(rateLimitWait)
 510
 511                    # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes
 512                    if 400 <= response.status_code < 500:
 513                        msg = "status code: [{}], response body: {}".format(response.status_code, response.text)
 514                        uLogger.debug("    - not oK, but do not retry for 4xx errors, {}".format(msg))
 515
 516                        if "code" in response.text and "message" in response.text:
 517                            msgDict = self._ParseJSON(rawData=response.text)
 518                            uLogger.warning("HTTP-status code [{}], server message: {}".format(response.status_code, msgDict["message"]))
 519
 520                        counter = retry + 1  # do not retry for 4xx errors
 521
 522                    if 500 <= response.status_code < 600:
 523                        errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text)
 524                        uLogger.debug("    - not oK, {}".format(errMsg))
 525
 526                        if "code" in response.text and "message" in response.text:
 527                            errMsgDict = self._ParseJSON(rawData=response.text)
 528                            uLogger.warning("HTTP-status code [{}], error message: {}".format(response.status_code, errMsgDict["message"]))
 529
 530                        counter += 1
 531
 532                        if counter <= retry:
 533                            uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause))
 534                            sleep(pause)
 535
 536                responseJSON = self._ParseJSON(rawData=response.text)
 537
 538                if errMsg:
 539                    uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/")
 540                    uLogger.error("    - not oK, {}".format(errMsg))
 541
 542        return responseJSON
 543
 544    def _IUpdater(self, iType: str) -> tuple:
 545        """
 546        Request instrument by type from server. See available API methods for instruments:
 547        Currencies: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Currencies
 548        Shares: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Shares
 549        Bonds: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Bonds
 550        Etfs: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Etfs
 551        Futures: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Futures
 552
 553        :param iType: type of the instrument, it must be one of supported types in TKS_INSTRUMENTS list.
 554        :return: tuple with iType name and list of available instruments of current type for defined user token.
 555        """
 556        result = []
 557
 558        if iType in TKS_INSTRUMENTS:
 559            uLogger.debug("Requesting available [{}] list. Wait, please...".format(iType))
 560
 561            # all instruments have the same body in API v2 requests:
 562            self.body = str({"instrumentStatus": "INSTRUMENT_STATUS_UNSPECIFIED"})  # Enum: [INSTRUMENT_STATUS_UNSPECIFIED, INSTRUMENT_STATUS_BASE, INSTRUMENT_STATUS_ALL]
 563            instrumentURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/{}".format(iType)
 564            result = self.SendAPIRequest(instrumentURL, reqType="POST")["instruments"]
 565
 566        return iType, result
 567
 568    def _IWrapper(self, kwargs):
 569        """
 570        Wrapper runs instrument's update method `_IUpdater()`.
 571        It's a workaround for using multiprocessing with kwargs. See: https://stackoverflow.com/a/36799206
 572        """
 573        return self._IUpdater(**kwargs)
 574
 575    def Listing(self) -> dict:
 576        """
 577        Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server.
 578
 579        :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures.
 580        """
 581        uLogger.debug("Requesting all available instruments for current account. Wait, please...")
 582        uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES))
 583
 584        # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService
 585        # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list.
 586        iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS]
 587
 588        poolUpdater = ThreadPool(processes=CPU_USAGES)  # create pool for update instruments in parallel mode
 589        listing = poolUpdater.map(self._IWrapper, iParams)  # execute update operations
 590        poolUpdater.close()  # close the thread pool
 591        poolUpdater.join()  # wait a moment until all data returns from threads
 592
 593        # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures.
 594        # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method
 595        iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing}
 596
 597        # calculate minimum price increment (step) for all instruments and set up instrument's type:
 598        for iType in iList.keys():
 599            for ticker in iList[iType]:
 600                iList[iType][ticker]["type"] = iType
 601
 602                if "minPriceIncrement" in iList[iType][ticker].keys():
 603                    iList[iType][ticker]["step"] = NanoToFloat(
 604                        iList[iType][ticker]["minPriceIncrement"]["units"],
 605                        iList[iType][ticker]["minPriceIncrement"]["nano"],
 606                    )
 607
 608                else:
 609                    iList[iType][ticker]["step"] = 0  # hack to avoid empty value in some instruments, e.g. futures
 610
 611        return iList
 612
 613    def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None:
 614        """
 615        Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics.
 616
 617        See also: `DumpInstruments()`, `Listing()`.
 618
 619        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
 620                            otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) .
 621        """
 622        if self.iListDumpFile is None or not self.iListDumpFile:
 623            uLogger.error("Output name of dump file must be defined!")
 624            raise Exception("Filename required")
 625
 626        if not self.iList or forceUpdate:
 627            self.iList = self.Listing()
 628
 629        xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx"
 630
 631        # Save as XLSX with separated sheets for every type of instruments:
 632        with pd.ExcelWriter(
 633                path=xlsxDumpFile,
 634                date_format=TKS_DATE_FORMAT,
 635                datetime_format=TKS_DATE_TIME_FORMAT,
 636                mode="w",
 637        ) as writer:
 638            for iType in TKS_INSTRUMENTS:
 639                df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index")  # generate pandas object from self.iList dictionary
 640                df = df[sorted(df)]  # sorted by column names
 641                df = df.applymap(
 642                    lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item,
 643                    na_action="ignore",
 644                )  # converting numbers from nano-type to float in every cell
 645                df.to_excel(
 646                    writer,
 647                    sheet_name=iType,
 648                    encoding="UTF-8",
 649                    freeze_panes=(1, 1),
 650                )  # saving as XLSX-file with freeze first row and column as headers
 651
 652        uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile)))
 653
 654    def DumpInstruments(self, forceUpdate: bool = True) -> str:
 655        """
 656        Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server
 657        using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file.
 658
 659        See also: `DumpInstrumentsAsXLSX()`, `Listing()`.
 660
 661        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
 662                            otherwise just saves exist `iList` as JSON-file (default: `dump.json`).
 663        :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file.
 664        """
 665        if self.iListDumpFile is None or not self.iListDumpFile:
 666            uLogger.error("Output name of dump file must be defined!")
 667            raise Exception("Filename required")
 668
 669        if not self.iList or forceUpdate:
 670            self.iList = self.Listing()
 671
 672        jsonDump = json.dumps(self.iList, indent=4, sort_keys=False)  # create JSON object as string
 673        with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH:
 674            fH.write(jsonDump)
 675
 676        uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile)))
 677
 678        return jsonDump
 679
 680    def ShowInstrumentInfo(self, iJSON: dict, show: bool = True, onlyFiles=False) -> str:
 681        """
 682        Show information about one instrument defined by json data and prints it in Markdown format.
 683
 684        See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`.
 685
 686        :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self._ticker]`
 687        :param show: if `True` then also printing information about instrument and its current price.
 688        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
 689        :return: multilines text in Markdown format with information about one instrument.
 690        """
 691        splitLine = "|                                                             |                                                        |\n"
 692        infoText = ""
 693
 694        if iJSON is not None and iJSON and isinstance(iJSON, dict):
 695            info = [
 696                "# Main information\n\n",
 697                "* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
 698                "| Parameters                                                  | Values                                                 |\n",
 699                "|-------------------------------------------------------------|--------------------------------------------------------|\n",
 700                "| Ticker:                                                     | {:<54} |\n".format(iJSON["ticker"]),
 701                "| Full name:                                                  | {:<54} |\n".format(iJSON["name"]),
 702            ]
 703
 704            if "sector" in iJSON.keys() and iJSON["sector"]:
 705                info.append("| Sector:                                                     | {:<54} |\n".format(iJSON["sector"]))
 706
 707            if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] and "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"]:
 708                info.append("| Country of instrument:                                      | {:<54} |\n".format("({}) {}".format(iJSON["countryOfRisk"], iJSON["countryOfRiskName"])))
 709
 710            info.extend([
 711                splitLine,
 712                "| FIGI (Financial Instrument Global Identifier):              | {:<54} |\n".format(iJSON["figi"]),
 713                "| Real exchange [Exchange section]:                           | {:<54} |\n".format("{} [{}]".format(TKS_REAL_EXCHANGES[iJSON["realExchange"]], iJSON["exchange"])),
 714            ])
 715
 716            if "isin" in iJSON.keys() and iJSON["isin"]:
 717                info.append("| ISIN (International Securities Identification Number):      | {:<54} |\n".format(iJSON["isin"]))
 718
 719            if "classCode" in iJSON.keys():
 720                info.append("| Class Code (exchange section where instrument is traded):   | {:<54} |\n".format(iJSON["classCode"]))
 721
 722            info.extend([
 723                splitLine,
 724                "| Current broker security trading status:                     | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]),
 725                splitLine,
 726                "| Buy operations allowed:                                     | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"),
 727                "| Sale operations allowed:                                    | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"),
 728                "| Short positions allowed:                                    | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"),
 729            ])
 730
 731            if iJSON["figi"]:
 732                self._figi = iJSON["figi"]
 733                iJSON = iJSON | self.RequestTradingStatus()
 734
 735                info.extend([
 736                    splitLine,
 737                    "| Limit orders allowed:                                       | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"),
 738                    "| Market orders allowed:                                      | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"),
 739                    "| API trade allowed:                                          | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"),
 740                ])
 741
 742            info.append(splitLine)
 743
 744            if "type" in iJSON.keys() and iJSON["type"]:
 745                info.append("| Type of the instrument:                                     | {:<54} |\n".format(iJSON["type"]))
 746
 747                if "shareType" in iJSON.keys() and iJSON["shareType"]:
 748                    info.append("| Share type:                                                 | {:<54} |\n".format(TKS_SHARE_TYPES[iJSON["shareType"]]))
 749
 750            if "futuresType" in iJSON.keys() and iJSON["futuresType"]:
 751                info.append("| Futures type:                                               | {:<54} |\n".format(iJSON["futuresType"]))
 752
 753            if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]:
 754                info.append("| IPO date:                                                   | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", "")))
 755
 756            if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]:
 757                info.append("| Released date:                                              | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", "")))
 758
 759            if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]:
 760                info.append("| Rebalancing frequency:                                      | {:<54} |\n".format(iJSON["rebalancingFreq"]))
 761
 762            if "focusType" in iJSON.keys() and iJSON["focusType"]:
 763                info.append("| Focusing type:                                              | {:<54} |\n".format(iJSON["focusType"]))
 764
 765            if "assetType" in iJSON.keys() and iJSON["assetType"]:
 766                info.append("| Asset type:                                                 | {:<54} |\n".format(iJSON["assetType"]))
 767
 768            if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]:
 769                info.append("| Basic asset:                                                | {:<54} |\n".format(iJSON["basicAsset"]))
 770
 771            if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]:
 772                info.append("| Basic asset size:                                           | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"]))))
 773
 774            if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]:
 775                info.append("| ISO currency name:                                          | {:<54} |\n".format(iJSON["isoCurrencyName"]))
 776
 777            if "currency" in iJSON.keys():
 778                info.append("| Payment currency:                                           | {:<54} |\n".format(iJSON["currency"]))
 779
 780            if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys():
 781                info.append("| Nominal currency:                                           | {:<54} |\n".format(iJSON["nominal"]["currency"]))
 782
 783            if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]:
 784                info.append("| First trade date:                                           | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", "")))
 785
 786            if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]:
 787                info.append("| Last trade date:                                            | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", "")))
 788
 789            if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]:
 790                info.append("| Date of expiration:                                         | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", "")))
 791
 792            if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]:
 793                info.append("| State registration date:                                    | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", "")))
 794
 795            if "placementDate" in iJSON.keys() and iJSON["placementDate"]:
 796                info.append("| Placement date:                                             | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", "")))
 797
 798            if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]:
 799                info.append("| Maturity date:                                              | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", "")))
 800
 801            if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]:
 802                info.append("| Perpetual bond:                                             | Yes                                                    |\n")
 803
 804            if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]:
 805                info.append("| Over-the-counter (OTC) securities:                          | Yes                                                    |\n")
 806
 807            iExt = None
 808            if iJSON["type"] == "Bonds":
 809                info.extend([
 810                    splitLine,
 811                    "| Bond issue (size / plan):                                   | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])),
 812                    "| Nominal price (100%):                                       | {:<54} |\n".format("{} {}".format(
 813                        "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."),
 814                        iJSON["nominal"]["currency"],
 815                    )),
 816                ])
 817
 818                if "floatingCouponFlag" in iJSON.keys():
 819                    info.append("| Floating coupon:                                            | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No"))
 820
 821                if "amortizationFlag" in iJSON.keys():
 822                    info.append("| Amortization:                                               | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No"))
 823
 824                info.append(splitLine)
 825
 826                if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]:
 827                    info.append("| Number of coupon payments per year:                         | {:<54} |\n".format(iJSON["couponQuantityPerYear"]))
 828
 829                if iJSON["figi"]:
 830                    iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False)  # extended bonds data
 831
 832                    info.extend([
 833                        "| Days last to maturity date:                                 | {:<54} |\n".format(iExt["daysToMaturity"][0]),
 834                        "| Coupons yield (average coupon daily yield * 365):           | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])),
 835                        "| Current price yield (average daily yield * 365):            | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])),
 836                    ])
 837
 838                if "aciValue" in iJSON.keys() and iJSON["aciValue"]:
 839                    info.append("| Current accumulated coupon income (ACI):                    | {:<54} |\n".format("{:.2f} {}".format(
 840                        NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]),
 841                        iJSON["aciValue"]["currency"]
 842                    )))
 843
 844            if "currentPrice" in iJSON.keys():
 845                info.append(splitLine)
 846
 847                currency = iJSON["currency"] if "currency" in iJSON.keys() else ""  # nominal currency for bonds, otherwise currency of instrument
 848                aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else ""  # payment currency
 849
 850                bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0  # previous close price of bond
 851                bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0  # last price of bond
 852                bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0  # max price of bond
 853                bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0  # min price of bond
 854                bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0  # delta between last deal price and last close
 855
 856                curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0
 857                curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0
 858
 859                info.extend([
 860                    "| Previous close price of the instrument:                     | {:<54} |\n".format("{}{}".format(
 861                        "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A",
 862                        "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
 863                    )),
 864                    "| Last deal price of the instrument:                          | {:<54} |\n".format("{}{}".format(
 865                        "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A",
 866                        "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
 867                    )),
 868                    "| Changes between last deal price and last close              | {:<54} |\n".format(
 869                        "{:.2f}%{}".format(
 870                            iJSON["currentPrice"]["changes"],
 871                            " ({}{:.2f} {})".format(
 872                                "+" if bondChangesDelta > 0 else "",
 873                                bondChangesDelta,
 874                                aciCurrency
 875                            ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format(
 876                                "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "",
 877                                iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"],
 878                                currency
 879                            ),
 880                        )
 881                    ),
 882                    "| Current limit price, min / max:                             | {:<54} |\n".format("{}{} / {}{}{}".format(
 883                        "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A",
 884                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 885                        "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A",
 886                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 887                        " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else ""
 888                    )),
 889                    "| Actual price, sell / buy:                                   | {:<54} |\n".format("{}{} / {}{}{}".format(
 890                        "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A",
 891                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 892                        "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A",
 893                        "%" if iJSON["type"] == "Bonds" else" {}".format(currency),
 894                        " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else ""
 895                    )),
 896                ])
 897
 898            if "lot" in iJSON.keys():
 899                info.append("| Minimum lot to buy:                                         | {:<54} |\n".format(iJSON["lot"]))
 900
 901            if "step" in iJSON.keys() and iJSON["step"] != 0:
 902                info.append("| Minimum price increment (step):                             | {:<54} |\n".format("{} {}".format(iJSON["step"], iJSON["currency"] if "currency" in iJSON.keys() else "")))
 903
 904            # Add bond payment calendar:
 905            if iJSON["type"] == "Bonds":
 906                strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False)   # bond payment calendar
 907                info.extend(["\n#", strCalendar])
 908
 909            infoText += "".join(info)
 910
 911            if show and not onlyFiles:
 912                uLogger.info("{}".format(infoText))
 913
 914            if self.infoFile is not None and (show or onlyFiles):
 915                with open(self.infoFile, "w", encoding="UTF-8") as fH:
 916                    fH.write(infoText)
 917
 918                uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile)))
 919
 920                if self.useHTMLReports:
 921                    htmlFilePath = self.infoFile.replace(".md", ".html") if self.infoFile.endswith(".md") else self.infoFile + ".html"
 922                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
 923                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Main information", commonCSS=COMMON_CSS, markdown=infoText))
 924
 925                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
 926
 927        return infoText
 928
 929    def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict:
 930        """
 931        Search and return raw broker's information about instrument by its ticker. Variable `ticker` must be defined!
 932
 933        :param requestPrice: if `False` then do not request current price of instrument (because this is long operation).
 934        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
 935        :return: JSON formatted data with information about instrument.
 936        """
 937        tickerJSON = {}
 938        if self.moreDebug:
 939            uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self._ticker))
 940
 941        if not self._ticker:
 942            uLogger.warning("self._ticker variable is not be empty!")
 943
 944        else:
 945            if self._ticker in TKS_TICKERS_OR_FIGI_EXCLUDED:
 946                uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self._ticker))
 947                raise Exception("Instrument not allowed")
 948
 949            if not self.iList:
 950                self.iList = self.Listing()
 951
 952            if self._ticker in self.iList["Shares"].keys():
 953                tickerJSON = self.iList["Shares"][self._ticker]
 954                if self.moreDebug:
 955                    uLogger.debug("Ticker [{}] found in shares list".format(self._ticker))
 956
 957            elif self._ticker in self.iList["Currencies"].keys():
 958                tickerJSON = self.iList["Currencies"][self._ticker]
 959                if self.moreDebug:
 960                    uLogger.debug("Ticker [{}] found in currencies list".format(self._ticker))
 961
 962            elif self._ticker in self.iList["Bonds"].keys():
 963                tickerJSON = self.iList["Bonds"][self._ticker]
 964                if self.moreDebug:
 965                    uLogger.debug("Ticker [{}] found in bonds list".format(self._ticker))
 966
 967            elif self._ticker in self.iList["Etfs"].keys():
 968                tickerJSON = self.iList["Etfs"][self._ticker]
 969                if self.moreDebug:
 970                    uLogger.debug("Ticker [{}] found in etfs list".format(self._ticker))
 971
 972            elif self._ticker in self.iList["Futures"].keys():
 973                tickerJSON = self.iList["Futures"][self._ticker]
 974                if self.moreDebug:
 975                    uLogger.debug("Ticker [{}] found in futures list".format(self._ticker))
 976
 977        if tickerJSON:
 978            self._figi = tickerJSON["figi"]
 979
 980            if requestPrice:
 981                tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False)
 982
 983                if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None:
 984                    tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"]
 985
 986                else:
 987                    tickerJSON["currentPrice"]["changes"] = 0
 988
 989            if show:
 990                self.ShowInstrumentInfo(iJSON=tickerJSON, show=True)  # print info as Markdown text
 991
 992        else:
 993            if show:
 994                uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self._ticker))
 995
 996        return tickerJSON
 997
 998    def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict:
 999        """
1000        Search and return raw broker's information about instrument by its FIGI. Variable `figi` must be defined!
1001
1002        :param requestPrice: if `False` then do not request current price of instrument (it's long operation).
1003        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
1004        :return: JSON formatted data with information about instrument.
1005        """
1006        figiJSON = {}
1007        if self.moreDebug:
1008            uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self._figi))
1009
1010        if not self._figi:
1011            uLogger.warning("self._figi variable is not be empty!")
1012
1013        else:
1014            if self._figi in TKS_TICKERS_OR_FIGI_EXCLUDED:
1015                uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self._figi))
1016                raise Exception("Instrument not allowed")
1017
1018            if not self.iList:
1019                self.iList = self.Listing()
1020
1021            for item in self.iList["Shares"].keys():
1022                if self._figi == self.iList["Shares"][item]["figi"]:
1023                    figiJSON = self.iList["Shares"][item]
1024
1025                    if self.moreDebug:
1026                        uLogger.debug("FIGI [{}] found in shares list".format(self._figi))
1027
1028                    break
1029
1030            if not figiJSON:
1031                for item in self.iList["Currencies"].keys():
1032                    if self._figi == self.iList["Currencies"][item]["figi"]:
1033                        figiJSON = self.iList["Currencies"][item]
1034
1035                        if self.moreDebug:
1036                            uLogger.debug("FIGI [{}] found in currencies list".format(self._figi))
1037
1038                        break
1039
1040            if not figiJSON:
1041                for item in self.iList["Bonds"].keys():
1042                    if self._figi == self.iList["Bonds"][item]["figi"]:
1043                        figiJSON = self.iList["Bonds"][item]
1044
1045                        if self.moreDebug:
1046                            uLogger.debug("FIGI [{}] found in bonds list".format(self._figi))
1047
1048                        break
1049
1050            if not figiJSON:
1051                for item in self.iList["Etfs"].keys():
1052                    if self._figi == self.iList["Etfs"][item]["figi"]:
1053                        figiJSON = self.iList["Etfs"][item]
1054
1055                        if self.moreDebug:
1056                            uLogger.debug("FIGI [{}] found in etfs list".format(self._figi))
1057
1058                        break
1059
1060            if not figiJSON:
1061                for item in self.iList["Futures"].keys():
1062                    if self._figi == self.iList["Futures"][item]["figi"]:
1063                        figiJSON = self.iList["Futures"][item]
1064
1065                        if self.moreDebug:
1066                            uLogger.debug("FIGI [{}] found in futures list".format(self._figi))
1067
1068                        break
1069
1070        if figiJSON:
1071            self._figi = figiJSON["figi"]
1072            self._ticker = figiJSON["ticker"]
1073
1074            if requestPrice:
1075                figiJSON["currentPrice"] = self.GetCurrentPrices(show=False)
1076
1077                if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None:
1078                    figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"]
1079
1080                else:
1081                    figiJSON["currentPrice"]["changes"] = 0
1082
1083            if show:
1084                self.ShowInstrumentInfo(iJSON=figiJSON, show=True)  # print info as Markdown text
1085
1086        else:
1087            if show:
1088                uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self._figi))
1089
1090        return figiJSON
1091
1092    def GetCurrentPrices(self, show: bool = True) -> dict:
1093        """
1094        Get and show Depth of Market with current prices of the instrument as dictionary. Result example with `depth` 5:
1095        `{"buy": [{"price": 1243.8, "quantity": 193},
1096                  {"price": 1244.0, "quantity": 168},
1097                  {"price": 1244.8, "quantity": 5},
1098                  {"price": 1245.0, "quantity": 61},
1099                  {"price": 1245.4, "quantity": 60}],
1100          "sell": [{"price": 1243.6, "quantity": 8},
1101                   {"price": 1242.6, "quantity": 10},
1102                   {"price": 1242.4, "quantity": 18},
1103                   {"price": 1242.2, "quantity": 50},
1104                   {"price": 1242.0, "quantity": 113}],
1105          "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}`, where parameters mean:
1106        - buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order
1107        - sell: list of dicts with Buyers prices,
1108            - price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument),
1109            - quantity: volume value by current price in lots,
1110        - limitUp: current trade session limit price, maximum,
1111        - limitDown: current trade session limit price, minimum,
1112        - lastPrice: last deal price of the instrument,
1113        - closePrice: previous trade session close price of the instrument.
1114
1115        See also: `SearchByTicker()` and `SearchByFIGI()`.
1116        REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1117        Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
1118
1119        :param show: if `True` then print DOM to log and console.
1120        :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`.
1121                 If an error occurred then returns an empty record:
1122                 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`.
1123        """
1124        prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0}
1125
1126        if self.depth < 1:
1127            uLogger.error("Depth of Market (DOM) must be >=1!")
1128            raise Exception("Incorrect value")
1129
1130        if not (self._ticker or self._figi):
1131            uLogger.error("self._ticker or self._figi variables must be defined!")
1132            raise Exception("Ticker or FIGI required")
1133
1134        if self._ticker and not self._figi:
1135            instrumentByTicker = self.SearchByTicker(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1136            self._figi = instrumentByTicker["figi"] if instrumentByTicker else ""
1137
1138        if not self._ticker and self._figi:
1139            instrumentByFigi = self.SearchByFIGI(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1140            self._ticker = instrumentByFigi["ticker"] if instrumentByFigi else ""
1141
1142        if not self._figi:
1143            uLogger.error("FIGI is not defined!")
1144            raise Exception("Ticker or FIGI required")
1145
1146        else:
1147            uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self._ticker, self._figi))
1148
1149            # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1150            priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook"
1151            self.body = str({"figi": self._figi, "depth": self.depth})
1152            pricesResponse = self.SendAPIRequest(priceURL, reqType="POST")  # Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
1153
1154            if pricesResponse and not ("code" in pricesResponse.keys() or "message" in pricesResponse.keys() or "description" in pricesResponse.keys()):
1155                # list of dicts with sellers orders:
1156                prices["buy"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]]
1157
1158                # list of dicts with buyers orders:
1159                prices["sell"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]]
1160
1161                # max price of instrument at this time:
1162                prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None
1163
1164                # min price of instrument at this time:
1165                prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None
1166
1167                # last price of deal with instrument:
1168                prices["lastPrice"] = round(NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]), 6) if "lastPrice" in pricesResponse.keys() else 0
1169
1170                # last close price of instrument:
1171                prices["closePrice"] = round(NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]), 6) if "closePrice" in pricesResponse.keys() else 0
1172
1173            else:
1174                uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi))
1175                uLogger.debug("Server response: {}".format(pricesResponse))
1176
1177            if show:
1178                if prices["buy"] or prices["sell"]:
1179                    info = [
1180                        "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format(
1181                            datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
1182                            self._ticker,
1183                            self._figi,
1184                            self.depth,
1185                        ),
1186                        "-" * 60, "\n",
1187                        "             Orders of Buyers | Orders of Sellers\n",
1188                        "-" * 60, "\n",
1189                        "        Sell prices (volumes) | Buy prices (volumes)\n",
1190                        "-" * 60, "\n",
1191                    ]
1192
1193                    if not prices["buy"]:
1194                        info.append("                              | No orders!\n")
1195                        sumBuy = 0
1196
1197                    else:
1198                        sumBuy = sum([x["quantity"] for x in prices["buy"]])
1199                        maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True)
1200                        for item in maxMinSorted:
1201                            info.append("                              | {} ({})\n".format(item["price"], item["quantity"]))
1202
1203                    if not prices["sell"]:
1204                        info.append("No orders!                    |\n")
1205                        sumSell = 0
1206
1207                    else:
1208                        sumSell = sum([x["quantity"] for x in prices["sell"]])
1209                        for item in prices["sell"]:
1210                            info.append("{:>29} |\n".format("{} ({})".format(item["price"], item["quantity"])))
1211
1212                    info.extend([
1213                        "-" * 60, "\n",
1214                        "{:>29} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)),
1215                        "-" * 60, "\n",
1216                    ])
1217
1218                    infoText = "".join(info)
1219
1220                    uLogger.info("Current prices in order book:\n\n{}".format(infoText))
1221
1222                else:
1223                    uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi))
1224
1225        return prices
1226
1227    def ShowInstrumentsInfo(self, show: bool = True, onlyFiles=False) -> str:
1228        """
1229        This method get and show information about all available broker instruments for current user account.
1230        If `instrumentsFile` string is not empty then also save information to this file.
1231
1232        :param show: if `True` then print results to console, if `False` — print only to file.
1233        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
1234        :return: multi-lines string with all available broker instruments.
1235        """
1236        if not self.iList:
1237            self.iList = self.Listing()
1238
1239        info = [
1240            "# All available instruments from Tinkoff Broker server for current user token\n\n",
1241            "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
1242        ]
1243
1244        # add instruments count by type:
1245        for iType in self.iList.keys():
1246            info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType])))
1247
1248        headerLine = "| Ticker       | Full name                                                 | FIGI         | Cur | Lot     | Step       |\n"
1249        splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n"
1250
1251        # generating info tables with all instruments by type:
1252        for iType in self.iList.keys():
1253            info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine])
1254
1255            for instrument in self.iList[iType].keys():
1256                iName = self.iList[iType][instrument]["name"]  # instrument's name
1257                if len(iName) > 57:
1258                    iName = "{}...".format(iName[:54])  # right trim for a long string
1259
1260                info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format(
1261                    self.iList[iType][instrument]["ticker"],
1262                    iName,
1263                    self.iList[iType][instrument]["figi"],
1264                    self.iList[iType][instrument]["currency"],
1265                    self.iList[iType][instrument]["lot"],
1266                    "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0,
1267                ))
1268
1269        infoText = "".join(info)
1270
1271        if show and not onlyFiles:
1272            uLogger.info(infoText)
1273
1274        if self.instrumentsFile and (show or onlyFiles):
1275            with open(self.instrumentsFile, "w", encoding="UTF-8") as fH:
1276                fH.write(infoText)
1277
1278            uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile)))
1279
1280            if self.useHTMLReports:
1281                htmlFilePath = self.instrumentsFile.replace(".md", ".html") if self.instrumentsFile.endswith(".md") else self.instrumentsFile + ".html"
1282                with open(htmlFilePath, "w", encoding="UTF-8") as fH:
1283                    fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="List of instruments", commonCSS=COMMON_CSS, markdown=infoText))
1284
1285                uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
1286
1287        return infoText
1288
1289    def SearchInstruments(self, pattern: str, show: bool = True, onlyFiles=False) -> dict:
1290        """
1291        This method search and show information about instruments by part of its ticker, FIGI or name.
1292        If `searchResultsFile` string is not empty then also save information to this file.
1293
1294        :param pattern: string with part of ticker, FIGI or instrument's name.
1295        :param show: if `True` then print results to console, if `False` — return list of result only.
1296        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
1297        :return: list of dictionaries with all found instruments.
1298        """
1299        if not self.iList:
1300            self.iList = self.Listing()
1301
1302        searchResults = {iType: {} for iType in self.iList}  # same as iList but will contain only filtered instruments
1303        compiledPattern = re.compile(pattern, re.IGNORECASE)
1304
1305        for iType in self.iList:
1306            for instrument in self.iList[iType].values():
1307                searchResult = compiledPattern.search(" ".join(
1308                    [instrument["ticker"], instrument["figi"], instrument["name"]]
1309                ))
1310
1311                if searchResult:
1312                    searchResults[iType][instrument["ticker"]] = instrument
1313
1314        resultsLen = sum([len(searchResults[iType]) for iType in searchResults])
1315        info = [
1316            "# Search results\n\n",
1317            "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
1318            "* **Search pattern:** [{}]\n".format(pattern),
1319            "* **Found instruments:** [{}]\n\n".format(resultsLen),
1320            '**Note:** you can view info about found instruments with key "--info", e.g.: "tksbrokerapi -t TICKER --info" or "tksbrokerapi -f FIGI --info".\n'
1321        ]
1322        infoShort = info[:]
1323
1324        headerLine = "| Type       | Ticker       | Full name                                                      | FIGI         |\n"
1325        splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n"
1326        skippedLine = "| ...        | ...          | ...                                                            | ...          |\n"
1327
1328        if resultsLen == 0:
1329            info.append("\nNo results\n")
1330            infoShort.append("\nNo results\n")
1331            uLogger.warning("No results. Try changing your search pattern.")
1332
1333        else:
1334            for iType in searchResults:
1335                iTypeValuesCount = len(searchResults[iType].values())
1336                if iTypeValuesCount > 0:
1337                    info.extend(["\n## {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1338                    infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1339
1340                    for instrument in searchResults[iType].values():
1341                        info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format(
1342                            instrument["type"],
1343                            instrument["ticker"],
1344                            "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"],  # right trim for a long string
1345                            instrument["figi"],
1346                        ))
1347
1348                    if iTypeValuesCount <= 5:
1349                        infoShort.extend(info[-iTypeValuesCount:])
1350
1351                    else:
1352                        infoShort.extend(info[-5:])
1353                        infoShort.append(skippedLine)
1354
1355        infoText = "".join(info)
1356        infoTextShort = "".join(infoShort)
1357
1358        if show and not onlyFiles:
1359            uLogger.info(infoTextShort)
1360            uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`")
1361
1362        if self.searchResultsFile and (show or onlyFiles):
1363            with open(self.searchResultsFile, "w", encoding="UTF-8") as fH:
1364                fH.write(infoText)
1365
1366            uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile)))
1367
1368            if self.useHTMLReports:
1369                htmlFilePath = self.searchResultsFile.replace(".md", ".html") if self.searchResultsFile.endswith(".md") else self.searchResultsFile + ".html"
1370                with open(htmlFilePath, "w", encoding="UTF-8") as fH:
1371                    fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Search results", commonCSS=COMMON_CSS, markdown=infoText))
1372
1373                uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
1374
1375        return searchResults
1376
1377    def GetUniqueFIGIs(self, instruments: list[str]) -> list:
1378        """
1379        Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs.
1380
1381        :param instruments: list of strings with tickers or FIGIs.
1382        :return: list with unique instrument FIGIs only.
1383        """
1384        requestedInstruments = []
1385        for iName in instruments:
1386            if iName not in self.aliases.keys():
1387                if iName not in requestedInstruments:
1388                    requestedInstruments.append(iName)
1389
1390            else:
1391                if iName not in requestedInstruments:
1392                    if self.aliases[iName] not in requestedInstruments:
1393                        requestedInstruments.append(self.aliases[iName])
1394
1395        uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments))
1396
1397        onlyUniqueFIGIs = []
1398        for iName in requestedInstruments:
1399            if iName in TKS_TICKERS_OR_FIGI_EXCLUDED:
1400                continue
1401
1402            self._ticker = iName
1403            iData = self.SearchByTicker(requestPrice=False)  # trying to find instrument by ticker
1404
1405            if not iData:
1406                self._ticker = ""
1407                self._figi = iName
1408
1409                iData = self.SearchByFIGI(requestPrice=False)  # trying to find instrument by FIGI
1410
1411                if not iData:
1412                    self._figi = ""
1413                    uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName))
1414
1415            if iData and iData["figi"] not in onlyUniqueFIGIs:
1416                onlyUniqueFIGIs.append(iData["figi"])
1417
1418        uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs))
1419
1420        return onlyUniqueFIGIs
1421
1422    def GetListOfPrices(self, instruments: list[str], show: bool = False, onlyFiles=False) -> list[dict]:
1423        """
1424        This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation!
1425
1426        See limits: https://tinkoff.github.io/investAPI/limits/
1427
1428        If `pricesFile` string is not empty then also save information to this file.
1429
1430        :param instruments: list of strings with tickers or FIGIs.
1431        :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`.
1432        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
1433        :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1434                 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods.
1435        """
1436        if instruments is None or not instruments:
1437            uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!")
1438            raise Exception("Ticker or FIGI required")
1439
1440        onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments)
1441
1442        uLogger.debug("Requesting current prices from Tinkoff Broker server...")
1443
1444        iList = []  # trying to get info and current prices about all unique instruments:
1445        for self._figi in onlyUniqueFIGIs:
1446            iData = self.SearchByFIGI(requestPrice=True, show=False)
1447            iList.append(iData)
1448
1449        self.ShowListOfPrices(iList, show, onlyFiles)
1450
1451        return iList
1452
1453    def ShowListOfPrices(self, iList: list, show: bool = True, onlyFiles=False) -> str:
1454        """
1455        Show table contains current prices of given instruments.
1456
1457        :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1458                      One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods.
1459        :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`.
1460        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
1461        :return: multilines text in Markdown format as a table contains current prices.
1462        """
1463        infoText = ""
1464
1465        if show or self.pricesFile or onlyFiles:
1466            info = [
1467                "# Current prices\n\n* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
1468                "| Ticker       | FIGI         | Type       | Prev. close | Last price  | Chg. %   | Day limits min/max  | Actual sell / buy   | Curr. |\n",
1469                "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n",
1470            ]
1471
1472            for item in iList:
1473                info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format(
1474                    item["ticker"],
1475                    item["figi"],
1476                    item["type"],
1477                    "{:.2f}".format(float(item["currentPrice"]["closePrice"])),
1478                    "{:.2f}".format(float(item["currentPrice"]["lastPrice"])),
1479                    "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])),
1480                    "{} / {}".format(
1481                        item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A",
1482                        item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A",
1483                    ),
1484                    "{} / {}".format(
1485                        item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A",
1486                        item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A",
1487                    ),
1488                    item["currency"],
1489                ))
1490
1491            infoText = "".join(info)
1492
1493            if show and not onlyFiles:
1494                uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText))
1495
1496            if self.pricesFile and (show or onlyFiles):
1497                with open(self.pricesFile, "w", encoding="UTF-8") as fH:
1498                    fH.write(infoText)
1499
1500                uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile)))
1501
1502                if self.useHTMLReports:
1503                    htmlFilePath = self.pricesFile.replace(".md", ".html") if self.pricesFile.endswith(".md") else self.pricesFile + ".html"
1504                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
1505                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Current prices", commonCSS=COMMON_CSS, markdown=infoText))
1506
1507                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
1508
1509        return infoText
1510
1511    def RequestTradingStatus(self) -> dict:
1512        """
1513        Requesting trading status for the instrument defined by `figi` variable.
1514
1515        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus
1516
1517        Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest
1518
1519        :return: dictionary with trading status attributes. Response example:
1520                 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING",
1521                  "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}`
1522        """
1523        if self._figi is None or not self._figi:
1524            uLogger.error("Variable `figi` must be defined for using this method!")
1525            raise Exception("FIGI required")
1526
1527        uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self._figi))
1528
1529        self.body = str({"figi": self._figi, "instrumentId": self._figi})
1530        tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus"
1531        tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST")
1532
1533        if self.moreDebug:
1534            uLogger.debug("Records about current trading status successfully received")
1535
1536        return tradingStatus
1537
1538    def RequestPortfolio(self) -> dict:
1539        """
1540        Requesting actual user's portfolio for current `accountId`.
1541
1542        REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio
1543
1544        Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest
1545
1546        :return: dictionary with user's portfolio.
1547        """
1548        if self.accountId is None or not self.accountId:
1549            uLogger.error("Variable `accountId` must be defined for using this method!")
1550            raise Exception("Account ID required")
1551
1552        uLogger.debug("Requesting current actual user's portfolio. Wait, please...")
1553
1554        self.body = str({"accountId": self.accountId})
1555        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio"
1556        rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST")
1557
1558        if self.moreDebug:
1559            uLogger.debug("Records about user's portfolio successfully received")
1560
1561        return rawPortfolio
1562
1563    def RequestPositions(self) -> dict:
1564        """
1565        Requesting open positions by currencies and instruments for current `accountId`.
1566
1567        REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions
1568
1569        Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest
1570
1571        :return: dictionary with open positions by instruments.
1572        """
1573        if self.accountId is None or not self.accountId:
1574            uLogger.error("Variable `accountId` must be defined for using this method!")
1575            raise Exception("Account ID required")
1576
1577        uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...")
1578
1579        self.body = str({"accountId": self.accountId})
1580        positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions"
1581        rawPositions = self.SendAPIRequest(positionsURL, reqType="POST")
1582
1583        if self.moreDebug:
1584            uLogger.debug("Records about current open positions successfully received")
1585
1586        return rawPositions
1587
1588    def RequestPendingOrders(self) -> list:
1589        """
1590        Requesting current actual pending limit orders for current `accountId`.
1591
1592        REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders
1593
1594        Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest
1595
1596        :return: list of dictionaries with pending limit orders.
1597        """
1598        if self.accountId is None or not self.accountId:
1599            uLogger.error("Variable `accountId` must be defined for using this method!")
1600            raise Exception("Account ID required")
1601
1602        uLogger.debug("Requesting current actual pending limit orders. Wait, please...")
1603
1604        self.body = str({"accountId": self.accountId})
1605        ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders"
1606        rawResponse = self.SendAPIRequest(ordersURL, reqType="POST")
1607
1608        if "orders" in rawResponse.keys():
1609            rawOrders = rawResponse["orders"]
1610            uLogger.debug("[{}] records about pending limit orders received".format(len(rawOrders)))
1611
1612        else:
1613            rawOrders = []
1614            uLogger.debug("No pending limit orders returned! rawResponse = {}".format(rawResponse))
1615
1616        return rawOrders
1617
1618    def RequestStopOrders(self) -> list:
1619        """
1620        Requesting current actual stop orders for current `accountId`.
1621
1622        REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders
1623
1624        Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest
1625
1626        :return: list of dictionaries with stop orders.
1627        """
1628        if self.accountId is None or not self.accountId:
1629            uLogger.error("Variable `accountId` must be defined for using this method!")
1630            raise Exception("Account ID required")
1631
1632        uLogger.debug("Requesting current actual stop orders. Wait, please...")
1633
1634        self.body = str({"accountId": self.accountId})
1635        stopOrdersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders"
1636        rawResponse = self.SendAPIRequest(stopOrdersURL, reqType="POST")
1637
1638        if "stopOrders" in rawResponse.keys():
1639            rawStopOrders = rawResponse["stopOrders"]
1640            uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders)))
1641
1642        else:
1643            rawStopOrders = []
1644            uLogger.debug("No stop orders returned! rawResponse = {}".format(rawResponse))
1645
1646        return rawStopOrders
1647
1648    def Overview(self, show: bool = False, details: str = "full", onlyFiles=False) -> dict:
1649        """
1650        Get portfolio: all open positions, orders and some statistics for current `accountId`.
1651        If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile`
1652        and `overviewBondsCalendarFile` are defined then also save information to file.
1653
1654        WARNING! It is not recommended to run this method too many times in a loop! The server receives
1655        many requests about the state of the portfolio, and then, based on the received data, a large number
1656        of calculation and statistics are collected.
1657
1658        :param show: if `False` then only dictionary returns, if `True` then show more debug information.
1659        :param details: how detailed should the information be?
1660        - `full` — shows full available information about portfolio status (by default),
1661        - `positions` — shows only open positions,
1662        - `orders` — shows only sections of open limits and stop orders.
1663        - `digest` — show a short digest of the portfolio status,
1664        - `analytics` — shows only the analytics section and the distribution of the portfolio by various categories,
1665        - `calendar` — shows only the bonds calendar section (if these present in portfolio).
1666        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
1667        :return: dictionary with client's raw portfolio and some statistics.
1668        """
1669        if self.accountId is None or not self.accountId:
1670            uLogger.error("Variable `accountId` must be defined for using this method!")
1671            raise Exception("Account ID required")
1672
1673        view = {
1674            "raw": {  # --- raw portfolio responses from broker with user portfolio data:
1675                "headers": {},  # list of dictionaries, response headers without "positions" section
1676                "Currencies": [],  # list of dictionaries, open trades with currencies from "positions" section
1677                "Shares": [],  # list of dictionaries, open trades with shares from "positions" section
1678                "Bonds": [],  # list of dictionaries, open trades with bonds from "positions" section
1679                "Etfs": [],  # list of dictionaries, open trades with etfs from "positions" section
1680                "Futures": [],  # list of dictionaries, open trades with futures from "positions" section
1681                "positions": {},  # raw response from broker: dictionary with current available or blocked currencies and instruments for client
1682                "orders": [],  # raw response from broker: list of dictionaries with all pending (market) orders
1683                "stopOrders": [],  # raw response from broker: list of dictionaries with all stop orders
1684                "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}},  # dict with prices of all currencies in RUB
1685            },
1686            "stat": {  # --- some statistics calculated using "raw" sections:
1687                "portfolioCostRUB": 0.,  # portfolio cost in RUB (Russian Rouble)
1688                "availableRUB": 0.,  # available rubles (without other currencies)
1689                "blockedRUB": 0.,  # blocked sum in Russian Rouble
1690                "totalChangesRUB": 0.,  # changes for all open trades in RUB
1691                "totalChangesPercentRUB": 0.,  # changes for all open trades in percents
1692                "allCurrenciesCostRUB": 0.,  # costs of all currencies (include rubles) in RUB
1693                "sharesCostRUB": 0.,  # costs of all shares in RUB
1694                "bondsCostRUB": 0.,  # costs of all bonds in RUB
1695                "etfsCostRUB": 0.,  # costs of all etfs in RUB
1696                "futuresCostRUB": 0.,  # costs of all futures in RUB
1697                "Currencies": [],  # list of dictionaries of all currencies statistics
1698                "Shares": [],  # list of dictionaries of all shares statistics
1699                "Bonds": [],  # list of dictionaries of all bonds statistics
1700                "Etfs": [],  # list of dictionaries of all etfs statistics
1701                "Futures": [],  # list of dictionaries of all futures statistics
1702                "orders": [],  # list of dictionaries of all pending (market) orders and it's parameters
1703                "stopOrders": [],  # list of dictionaries of all stop orders and it's parameters
1704                "blockedCurrencies": {},  # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21}
1705                "blockedInstruments": {},  # dict with blocked  by FIGI, e.g. {}
1706                "funds": {},  # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1707            },
1708            "analytics": {  # --- some analytics of portfolio:
1709                "distrByAssets": {},  # portfolio distribution by assets
1710                "distrByCompanies": {},  # portfolio distribution by companies
1711                "distrBySectors": {},  # portfolio distribution by sectors
1712                "distrByCurrencies": {},  # portfolio distribution by currencies
1713                "distrByCountries": {},  # portfolio distribution by countries
1714                "bondsCalendar": None,  # bonds payment calendar as Pandas DataFrame (if these present in portfolio)
1715            }
1716        }
1717
1718        details = details.lower()
1719        availableDetails = ["full", "positions", "orders", "analytics", "calendar", "digest"]
1720        if details not in availableDetails:
1721            details = "full"
1722            uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails))
1723
1724        uLogger.debug("Requesting portfolio of a client. Wait, please...")
1725
1726        portfolioResponse = self.RequestPortfolio()  # current user's portfolio (dict)
1727        view["raw"]["positions"] = self.RequestPositions()  # current open positions by instruments (dict)
1728        view["raw"]["orders"] = self.RequestPendingOrders()  # current actual pending limit orders (list)
1729        view["raw"]["stopOrders"] = self.RequestStopOrders()  # current actual stop orders (list)
1730
1731        # save response headers without "positions" section:
1732        for key in portfolioResponse.keys():
1733            if key != "positions":
1734                view["raw"]["headers"][key] = portfolioResponse[key]
1735
1736            else:
1737                continue
1738
1739        # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation
1740        # Type of instrument must be only one of supported types in TKS_INSTRUMENTS
1741        for item in portfolioResponse["positions"]:
1742            if item["instrumentType"] == "currency":
1743                self._figi = item["figi"]
1744                if not self._figi and item["ticker"]:
1745                    self._ticker = item["ticker"]
1746                    self._figi = self.SearchByTicker()["figi"]  # Get FIGI to avoid warnings
1747
1748                curr = self.SearchByFIGI(requestPrice=False)
1749
1750                # current price of currency in RUB:
1751                view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = {
1752                    "name": curr["name"],
1753                    "currentPrice": NanoToFloat(
1754                        item["currentPrice"]["units"],
1755                        item["currentPrice"]["nano"]
1756                    ),
1757                }
1758
1759                view["raw"]["Currencies"].append(item)
1760
1761            elif item["instrumentType"] == "share":
1762                view["raw"]["Shares"].append(item)
1763
1764            elif item["instrumentType"] == "bond":
1765                view["raw"]["Bonds"].append(item)
1766
1767            elif item["instrumentType"] == "etf":
1768                view["raw"]["Etfs"].append(item)
1769
1770            elif item["instrumentType"] == "futures":
1771                view["raw"]["Futures"].append(item)
1772
1773            else:
1774                continue
1775
1776        # how many volume of currencies (by ISO currency name) are blocked:
1777        for item in view["raw"]["positions"]["blocked"]:
1778            blocked = NanoToFloat(item["units"], item["nano"])
1779            if blocked > 0:
1780                view["stat"]["blockedCurrencies"][item["currency"]] = blocked
1781
1782        # how many volume of instruments (by FIGI) are blocked:
1783        for item in view["raw"]["positions"]["securities"]:
1784            blocked = int(item["blocked"])
1785            if blocked > 0:
1786                view["stat"]["blockedInstruments"][item["figi"]] = blocked
1787
1788        allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]}
1789
1790        if "rub" in allBlocked.keys():
1791            view["stat"]["blockedRUB"] = allBlocked["rub"]  # blocked rubles
1792
1793        # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies:
1794        view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"])
1795        view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"])
1796        view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"])
1797        view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"])
1798        view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"])
1799        view["stat"]["portfolioCostRUB"] = sum([
1800            view["stat"]["allCurrenciesCostRUB"],
1801            view["stat"]["sharesCostRUB"],
1802            view["stat"]["bondsCostRUB"],
1803            view["stat"]["etfsCostRUB"],
1804            view["stat"]["futuresCostRUB"],
1805        ])
1806
1807        # --- calculating some portfolio statistics:
1808        byComp = {}  # distribution by companies
1809        bySect = {}  # distribution by sectors
1810        byCurr = {}  # distribution by currencies (include RUB)
1811        unknownCountryName = "All other countries"  # default name for instruments without "countryOfRisk" and "countryOfRiskName"
1812        byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}}  # distribution by countries (currencies are included in their countries)
1813
1814        for item in portfolioResponse["positions"]:
1815            self._figi = item["figi"]
1816            if not self._figi and item["ticker"]:
1817                self._ticker = item["ticker"]
1818                self._figi = self.SearchByTicker()["figi"]  # Get FIGI to avoid warnings
1819
1820            instrument = self.SearchByFIGI(requestPrice=False)  # full raw info about instrument by FIGI
1821
1822            if instrument:
1823                if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys():
1824                    blocked = allBlocked[instrument["nominal"]["currency"]]  # blocked volume of currency
1825
1826                elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys():
1827                    blocked = allBlocked[item["figi"]]  # blocked volume of other instruments
1828
1829                else:
1830                    blocked = 0
1831
1832                volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"])  # available volume of instrument
1833                lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"])  # available volume in lots of instrument
1834                direction = "Long" if lots >= 0 else "Short"  # direction of an instrument's position: short or long
1835                curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"])  # current instrument's price
1836                average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"])  # current average position price
1837                profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"])  # expected profit at current moment
1838                currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"]  # currency name rub, usd, eur etc.
1839                cost = curPrice if "currentNkd" not in item.keys() else (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume  # current cost of all volume of instrument in basic asset
1840                baseCurrencyName = item["currentPrice"]["currency"]  # name of base currency (rub)
1841                countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName
1842                costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"]  # cost in rubles
1843                percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.  # instrument's part in percent of full portfolio cost
1844
1845                statData = {
1846                    "figi": item["figi"],  # FIGI from REST API "GetPortfolio" method
1847                    "ticker": instrument["ticker"],  # ticker by FIGI
1848                    "currency": currency,  # currency name rub, usd, eur etc. for instrument price
1849                    "volume": volume,  # available volume of instrument
1850                    "lots": lots,  # volume in lots of instrument
1851                    "direction": direction,  # direction of an instrument's position: short or long
1852                    "blocked": blocked,  # blocked volume of currency or instrument
1853                    "currentPrice": curPrice,  # current instrument's price in basic asset
1854                    "average": average,  # current average position price
1855                    "cost": cost,  # current cost of all volume of instrument in basic asset
1856                    "baseCurrencyName": baseCurrencyName,  # name of base currency (rub)
1857                    "costRUB": costRUB,  # cost of instrument in ruble
1858                    "percentCostRUB": percentCostRUB,  # instrument's part in percent of full portfolio cost in RUB
1859                    "profit": profit,  # expected profit at current moment
1860                    "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0,  # expected percents of profit at current moment for this instrument
1861                    "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other",
1862                    "name": instrument["name"] if "name" in instrument.keys() else "",  # human-readable names of instruments
1863                    "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "",  # ISO name for currencies only
1864                    "country": countryName,  # e.g. "[RU] Российская Федерация" or unknownCountryName
1865                    "step": instrument["step"],  # minimum price increment
1866                }
1867
1868                # adding distribution by unique countries:
1869                if statData["country"] not in byCountry.keys():
1870                    byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB}
1871
1872                else:
1873                    byCountry[statData["country"]]["cost"] += costRUB
1874                    byCountry[statData["country"]]["percent"] += percentCostRUB
1875
1876                if item["instrumentType"] != "currency":
1877                    # adding distribution by unique companies:
1878                    if statData["name"]:
1879                        if statData["name"] not in byComp.keys():
1880                            byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB}
1881
1882                        else:
1883                            byComp[statData["name"]]["cost"] += costRUB
1884                            byComp[statData["name"]]["percent"] += percentCostRUB
1885
1886                    # adding distribution by unique sectors:
1887                    if statData["sector"] not in bySect.keys():
1888                        bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB}
1889
1890                    else:
1891                        bySect[statData["sector"]]["cost"] += costRUB
1892                        bySect[statData["sector"]]["percent"] += percentCostRUB
1893
1894                # adding distribution by unique currencies:
1895                if currency not in byCurr.keys():
1896                    byCurr[currency] = {
1897                        "name": view["raw"]["currenciesCurrentPrices"][currency]["name"],
1898                        "cost": costRUB,
1899                        "percent": percentCostRUB
1900                    }
1901
1902                else:
1903                    byCurr[currency]["cost"] += costRUB
1904                    byCurr[currency]["percent"] += percentCostRUB
1905
1906                # saving statistics for every instrument:
1907                if item["instrumentType"] == "currency":
1908                    view["stat"]["Currencies"].append(statData)
1909
1910                    # update dict with free funds for trading (total - blocked) by currencies
1911                    # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1912                    view["stat"]["funds"][currency] = {
1913                        "total": volume,
1914                        "totalCostRUB": costRUB,  # total volume cost in rubles
1915                        "free": volume - blocked,
1916                        "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0,  # free volume cost in rubles
1917                    }
1918
1919                elif item["instrumentType"] == "share":
1920                    view["stat"]["Shares"].append(statData)
1921
1922                elif item["instrumentType"] == "bond":
1923                    view["stat"]["Bonds"].append(statData)
1924
1925                elif item["instrumentType"] == "etf":
1926                    view["stat"]["Etfs"].append(statData)
1927
1928                elif item["instrumentType"] == "Futures":
1929                    view["stat"]["Futures"].append(statData)
1930
1931                else:
1932                    continue
1933
1934        # total changes in Russian Ruble:
1935        view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]])  # available RUB without other currencies
1936        view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0.
1937        startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100)
1938        view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost
1939        view["stat"]["funds"]["rub"] = {
1940            "total": view["stat"]["availableRUB"],
1941            "totalCostRUB": view["stat"]["availableRUB"],
1942            "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1943            "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1944        }
1945
1946        # --- pending limit orders sector data:
1947        uniquePendingOrdersFIGIs = []  # unique FIGIs of pending limit orders to avoid many times price requests
1948        uniquePendingOrders = {}  # unique instruments with FIGIs as dictionary keys
1949
1950        for item in view["raw"]["orders"]:
1951            self._figi = item["figi"]
1952
1953            if item["figi"] not in uniquePendingOrdersFIGIs:
1954                instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI, price requests only one time
1955
1956                uniquePendingOrdersFIGIs.append(item["figi"])
1957                uniquePendingOrders[item["figi"]] = instrument
1958
1959            else:
1960                instrument = uniquePendingOrders[item["figi"]]
1961
1962            if instrument:
1963                action = TKS_ORDER_DIRECTIONS[item["direction"]]
1964                orderType = TKS_ORDER_TYPES[item["orderType"]]
1965                orderState = TKS_ORDER_STATES[item["executionReportStatus"]]
1966                orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1967
1968                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1969                if item["direction"] == "ORDER_DIRECTION_BUY":
1970                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
1971
1972                else:
1973                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
1974
1975                # requested price for order execution:
1976                target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"])
1977
1978                # necessary changes in percent to reach target from current price:
1979                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
1980
1981                view["stat"]["orders"].append({
1982                    "orderID": item["orderId"],  # orderId number parameter of current order
1983                    "figi": item["figi"],  # FIGI identification
1984                    "ticker": instrument["ticker"],  # ticker name by FIGI
1985                    "lotsRequested": item["lotsRequested"],  # requested lots value
1986                    "lotsExecuted": item["lotsExecuted"],  # how many lots are executed
1987                    "currentPrice": lastPrice,  # current instrument's price for defined action
1988                    "targetPrice": target,  # requested price for order execution in base currency
1989                    "baseCurrencyName": item["initialSecurityPrice"]["currency"],  # name of base currency
1990                    "percentChanges": changes,  # changes in percent to target from current price
1991                    "currency": item["currency"],  # instrument's currency name
1992                    "action": action,  # sell / buy / Unknown from TKS_ORDER_DIRECTIONS
1993                    "type": orderType,  # type of order from TKS_ORDER_TYPES
1994                    "status": orderState,  # order status from TKS_ORDER_STATES
1995                    "date": orderDate,  # string with order date and time from UTC format (without nano seconds part)
1996                })
1997
1998        # --- stop orders sector data:
1999        uniqueStopOrdersFIGIs = []  # unique FIGIs of stop orders to avoid many times price requests
2000        uniqueStopOrders = {}  # unique instruments with FIGIs as dictionary keys
2001
2002        for item in view["raw"]["stopOrders"]:
2003            self._figi = item["figi"]
2004
2005            if item["figi"] not in uniqueStopOrdersFIGIs:
2006                instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI, price requests only one time
2007
2008                uniqueStopOrdersFIGIs.append(item["figi"])
2009                uniqueStopOrders[item["figi"]] = instrument
2010
2011            else:
2012                instrument = uniqueStopOrders[item["figi"]]
2013
2014            if instrument:
2015                action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]]
2016                orderType = TKS_STOP_ORDER_TYPES[item["orderType"]]
2017                createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
2018
2019                # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order
2020                if "expirationTime" in item.keys():
2021                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"]
2022                    expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0]
2023
2024                else:
2025                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"]
2026                    expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"]
2027
2028                # current instrument's price (last sellers order if buy, and last buyers order if sell):
2029                if item["direction"] == "STOP_ORDER_DIRECTION_BUY":
2030                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
2031
2032                else:
2033                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
2034
2035                # requested price when stop-order executed:
2036                target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"])
2037
2038                # price for limit-order, set up when stop-order executed:
2039                limit = NanoToFloat(item["price"]["units"], item["price"]["nano"])
2040
2041                # necessary changes in percent to reach target from current price:
2042                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
2043
2044                view["stat"]["stopOrders"].append({
2045                    "orderID": item["stopOrderId"],  # stopOrderId number parameter of current stop-order
2046                    "figi": item["figi"],  # FIGI identification
2047                    "ticker": instrument["ticker"],  # ticker name by FIGI
2048                    "lotsRequested": item["lotsRequested"],  # requested lots value
2049                    "currentPrice": lastPrice,  # current instrument's price for defined action
2050                    "targetPrice": target,  # requested price for stop-order execution in base currency
2051                    "limitPrice": limit,  # price for limit-order, set up when stop-order executed, 0 if market order
2052                    "baseCurrencyName": item["stopPrice"]["currency"],  # name of base currency
2053                    "percentChanges": changes,  # changes in percent to target from current price
2054                    "currency": item["currency"],  # instrument's currency name
2055                    "action": action,  # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS
2056                    "type": orderType,  # type of order from TKS_STOP_ORDER_TYPES
2057                    "expType": expType,  # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES
2058                    "createDate": createDate,  # string with created order date and time from UTC format (without nano seconds part)
2059                    "expDate": expDate,  # string with expiration order date and time from UTC format (without nano seconds part)
2060                })
2061
2062        # --- calculating data for analytics section:
2063        # portfolio distribution by assets:
2064        view["analytics"]["distrByAssets"] = {
2065            "Ruble": {
2066                "uniques": 1,
2067                "cost": view["stat"]["availableRUB"],
2068                "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2069            },
2070            "Currencies": {
2071                "uniques": len(view["stat"]["Currencies"]),  # all foreign currencies without RUB
2072                "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"],
2073                "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2074            },
2075            "Shares": {
2076                "uniques": len(view["stat"]["Shares"]),
2077                "cost": view["stat"]["sharesCostRUB"],
2078                "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2079            },
2080            "Bonds": {
2081                "uniques": len(view["stat"]["Bonds"]),
2082                "cost": view["stat"]["bondsCostRUB"],
2083                "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2084            },
2085            "Etfs": {
2086                "uniques": len(view["stat"]["Etfs"]),
2087                "cost": view["stat"]["etfsCostRUB"],
2088                "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2089            },
2090            "Futures": {
2091                "uniques": len(view["stat"]["Futures"]),
2092                "cost": view["stat"]["futuresCostRUB"],
2093                "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2094            },
2095        }
2096
2097        # portfolio distribution by companies:
2098        view["analytics"]["distrByCompanies"]["All money cash"] = {
2099            "ticker": "",
2100            "cost": view["stat"]["allCurrenciesCostRUB"],
2101            "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2102        }
2103        view["analytics"]["distrByCompanies"].update(byComp)
2104
2105        # portfolio distribution by sectors:
2106        view["analytics"]["distrBySectors"]["All money cash"] = {
2107            "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"],
2108            "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"],
2109        }
2110        view["analytics"]["distrBySectors"].update(bySect)
2111
2112        # portfolio distribution by currencies:
2113        if "rub" not in view["analytics"]["distrByCurrencies"].keys():
2114            view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0}
2115
2116            if self.moreDebug:
2117                uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!")
2118
2119        view["analytics"]["distrByCurrencies"].update(byCurr)
2120        view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
2121        view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
2122
2123        # portfolio distribution by countries:
2124        if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys():
2125            view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0}
2126
2127            if self.moreDebug:
2128                uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!")
2129
2130        view["analytics"]["distrByCountries"].update(byCountry)
2131        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
2132        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
2133
2134        # --- Prepare text statistics overview in human-readable:
2135        if show or onlyFiles:
2136            actualOnDate = datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)
2137
2138            # Whatever the value `details`, header not changes:
2139            info = [
2140                "# Client's portfolio\n\n",
2141                "* **Actual on date:** [{} UTC]\n".format(actualOnDate),
2142                "* **Account ID:** [{}]\n".format(self.accountId),
2143            ]
2144
2145            if details in ["full", "positions", "digest"]:
2146                info.extend([
2147                    "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2148                    "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format(
2149                        "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2150                        view["stat"]["totalChangesRUB"],
2151                        "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2152                        view["stat"]["totalChangesPercentRUB"],
2153                    ),
2154                ])
2155
2156            if details in ["full", "positions"]:
2157                info.extend([
2158                    "## Open positions\n\n",
2159                    "| Ticker [FIGI]               | Volume (blocked)                | Lots     | Curr. price  | Avg. price   | Current volume cost | Profit (%)                   |\n",
2160                    "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n",
2161                    "| **Ruble:**                  | {:>31} |          |              |              |                     |                              |\n".format(
2162                        "{:.2f} ({:.2f}) rub".format(
2163                            view["stat"]["availableRUB"],
2164                            view["stat"]["blockedRUB"],
2165                        )
2166                    )
2167                ])
2168
2169                def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list:
2170                    return [
2171                        "|                             |                                 |          |              |              |                     |                              |\n",
2172                        "| {:<27} |                                 |          |              |              | {:>19} |                              |\n".format(
2173                            noTradeStr if noTradeStr else typeStr,
2174                            "" if noTradeStr else "{:.2f} RUB".format(CostRUB),
2175                        ),
2176                    ]
2177
2178                def _InfoStr(data: dict, isCurr: bool = False) -> str:
2179                    return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format(
2180                        "{} [{}]".format(data["ticker"], data["figi"]),
2181                        "{:.2f} ({:.2f}) {}".format(
2182                            data["volume"],
2183                            data["blocked"],
2184                            data["currency"],
2185                        ) if isCurr else "{:.0f} ({:.0f})".format(
2186                            data["volume"],
2187                            data["blocked"],
2188                        ),
2189                        "—" if isCurr else "{:.4f}".format(data["lots"]).rstrip("0").rstrip("."),
2190                        "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a",
2191                        "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a",
2192                        "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]),
2193                        "{}{:.2f} {} ({}{:.2f}%)".format(
2194                            "+" if data["profit"] > 0 else "",
2195                            data["profit"], data["baseCurrencyName"],
2196                            "+" if data["percentProfit"] > 0 else "",
2197                            data["percentProfit"],
2198                        ),
2199                    )
2200
2201                # --- Show currencies section:
2202                if view["stat"]["Currencies"]:
2203                    info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**"))
2204                    for item in view["stat"]["Currencies"]:
2205                        info.append(_InfoStr(item, isCurr=True))
2206
2207                else:
2208                    info.extend(_SplitStr(noTradeStr="**Currencies:** no trades"))
2209
2210                # --- Show shares section:
2211                if view["stat"]["Shares"]:
2212                    info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**"))
2213
2214                    for item in view["stat"]["Shares"]:
2215                        info.append(_InfoStr(item))
2216
2217                else:
2218                    info.extend(_SplitStr(noTradeStr="**Shares:** no trades"))
2219
2220                # --- Show bonds section:
2221                if view["stat"]["Bonds"]:
2222                    info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**"))
2223
2224                    for item in view["stat"]["Bonds"]:
2225                        info.append(_InfoStr(item))
2226
2227                else:
2228                    info.extend(_SplitStr(noTradeStr="**Bonds:** no trades"))
2229
2230                # --- Show etfs section:
2231                if view["stat"]["Etfs"]:
2232                    info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**"))
2233
2234                    for item in view["stat"]["Etfs"]:
2235                        info.append(_InfoStr(item))
2236
2237                else:
2238                    info.extend(_SplitStr(noTradeStr="**Etfs:** no trades"))
2239
2240                # --- Show futures section:
2241                if view["stat"]["Futures"]:
2242                    info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**"))
2243
2244                    for item in view["stat"]["Futures"]:
2245                        info.append(_InfoStr(item))
2246
2247                else:
2248                    info.extend(_SplitStr(noTradeStr="**Futures:** no trades"))
2249
2250            if details in ["full", "orders"]:
2251                # --- Show pending limit orders section:
2252                if view["stat"]["orders"]:
2253                    info.extend([
2254                        "\n## Opened pending limit-orders: [{}]\n".format(len(view["stat"]["orders"])),
2255                        "\n| Ticker [FIGI]               | Order ID       | Lots (exec.) | Current price (% delta) | Target price  | Action    | Type      | Create date (UTC)       |\n",
2256                        "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n",
2257                    ])
2258
2259                    for item in view["stat"]["orders"]:
2260                        info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format(
2261                            "{} [{}]".format(item["ticker"], item["figi"]),
2262                            item["orderID"],
2263                            "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]),
2264                            "{} {} ({}{:.2f}%)".format(
2265                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2266                                item["baseCurrencyName"],
2267                                "+" if item["percentChanges"] > 0 else "",
2268                                float(item["percentChanges"]),
2269                            ),
2270                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2271                            item["action"],
2272                            item["type"],
2273                            item["date"],
2274                        ))
2275
2276                else:
2277                    info.append("\n## Total pending limit-orders: [0]\n")
2278
2279                # --- Show stop orders section:
2280                if view["stat"]["stopOrders"]:
2281                    info.extend([
2282                        "\n## Opened stop-orders: [{}]\n".format(len(view["stat"]["stopOrders"])),
2283                        "\n| Ticker [FIGI]               | Stop order ID                        | Lots   | Current price (% delta) | Target price  | Limit price   | Action    | Type        | Expire type  | Create date (UTC)   | Expiration (UTC)    |\n",
2284                        "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n",
2285                    ])
2286
2287                    for item in view["stat"]["stopOrders"]:
2288                        info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format(
2289                            "{} [{}]".format(item["ticker"], item["figi"]),
2290                            item["orderID"],
2291                            item["lotsRequested"],
2292                            "{} {} ({}{:.2f}%)".format(
2293                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2294                                item["baseCurrencyName"],
2295                                "+" if item["percentChanges"] > 0 else "",
2296                                float(item["percentChanges"]),
2297                            ),
2298                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2299                            "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"],
2300                            item["action"],
2301                            item["type"],
2302                            item["expType"],
2303                            item["createDate"],
2304                            item["expDate"],
2305                        ))
2306
2307                else:
2308                    info.append("\n## Total stop-orders: [0]\n")
2309
2310            if details in ["full", "analytics"]:
2311                # -- Show analytics section:
2312                if view["stat"]["portfolioCostRUB"] > 0:
2313                    info.extend([
2314                        "\n# Analytics\n\n"
2315                        "* **Actual on date:** [{} UTC]\n".format(actualOnDate),
2316                        "* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2317                        "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format(
2318                            "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2319                            view["stat"]["totalChangesRUB"],
2320                            "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2321                            view["stat"]["totalChangesPercentRUB"],
2322                        ),
2323                        "\n## Portfolio distribution by assets\n"
2324                        "\n| Type                               | Uniques | Percent | Current cost       |\n",
2325                        "|------------------------------------|---------|---------|--------------------|\n",
2326                    ])
2327
2328                    for key in view["analytics"]["distrByAssets"].keys():
2329                        if view["analytics"]["distrByAssets"][key]["cost"] > 0:
2330                            info.append("| {:<34} | {:<7} | {:<7} | {:<18} |\n".format(
2331                                key,
2332                                view["analytics"]["distrByAssets"][key]["uniques"],
2333                                "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]),
2334                                "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]),
2335                            ))
2336
2337                    aSepLine = "|----------------------------------------------|---------|--------------------|\n"
2338
2339                    info.extend([
2340                        "\n## Portfolio distribution by companies\n"
2341                        "\n| Company                                      | Percent | Current cost       |\n",
2342                        aSepLine,
2343                    ])
2344
2345                    for company in view["analytics"]["distrByCompanies"].keys():
2346                        if view["analytics"]["distrByCompanies"][company]["cost"] > 0:
2347                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2348                                "{}{}".format(
2349                                    "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "",
2350                                    company,
2351                                ),
2352                                "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]),
2353                                "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]),
2354                            ))
2355
2356                    info.extend([
2357                        "\n## Portfolio distribution by sectors\n"
2358                        "\n| Sector                                       | Percent | Current cost       |\n",
2359                        aSepLine,
2360                    ])
2361
2362                    for sector in view["analytics"]["distrBySectors"].keys():
2363                        if view["analytics"]["distrBySectors"][sector]["cost"] > 0:
2364                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2365                                sector,
2366                                "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]),
2367                                "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]),
2368                            ))
2369
2370                    info.extend([
2371                        "\n## Portfolio distribution by currencies\n"
2372                        "\n| Instruments currencies                       | Percent | Current cost       |\n",
2373                        aSepLine,
2374                    ])
2375
2376                    for curr in view["analytics"]["distrByCurrencies"].keys():
2377                        if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0:
2378                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2379                                "[{}] {}".format(curr, view["analytics"]["distrByCurrencies"][curr]["name"]),
2380                                "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]),
2381                                "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]),
2382                            ))
2383
2384                    info.extend([
2385                        "\n## Portfolio distribution by countries\n"
2386                        "\n| Assets by country                            | Percent | Current cost       |\n",
2387                        aSepLine,
2388                    ])
2389
2390                    for country in view["analytics"]["distrByCountries"].keys():
2391                        if view["analytics"]["distrByCountries"][country]["cost"] > 0:
2392                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2393                                country,
2394                                "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]),
2395                                "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]),
2396                            ))
2397
2398            if details in ["full", "calendar"]:
2399                # -- Show bonds payment calendar section:
2400                if view["stat"]["Bonds"]:
2401                    bondTickers = [item["ticker"] for item in view["stat"]["Bonds"]]
2402                    view["analytics"]["bondsCalendar"] = self.ExtendBondsData(instruments=bondTickers, xlsx=False)
2403                    info.append("\n" + self.ShowBondsCalendar(extBonds=view["analytics"]["bondsCalendar"], show=False))
2404
2405                else:
2406                    info.append("\n# Bond payments calendar\n\nNo bonds in the portfolio to create payments calendar\n")
2407
2408            infoText = "".join(info)
2409
2410            if show and not onlyFiles:
2411                uLogger.info(infoText)
2412
2413            if details == "full" and self.overviewFile:
2414                filename = self.overviewFile
2415
2416            elif details == "digest" and self.overviewDigestFile:
2417                filename = self.overviewDigestFile
2418
2419            elif details == "positions" and self.overviewPositionsFile:
2420                filename = self.overviewPositionsFile
2421
2422            elif details == "orders" and self.overviewOrdersFile:
2423                filename = self.overviewOrdersFile
2424
2425            elif details == "analytics" and self.overviewAnalyticsFile:
2426                filename = self.overviewAnalyticsFile
2427
2428            elif details == "calendar" and self.overviewBondsCalendarFile:
2429                filename = self.overviewBondsCalendarFile
2430
2431            else:
2432                filename = ""
2433
2434            if filename and (show or onlyFiles):
2435                with open(filename, "w", encoding="UTF-8") as fH:
2436                    fH.write(infoText)
2437
2438                uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename)))
2439
2440                if self.useHTMLReports:
2441                    htmlFilePath = filename.replace(".md", ".html") if filename.endswith(".md") else filename + ".html"
2442                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
2443                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Client's portfolio", commonCSS=COMMON_CSS, markdown=infoText))
2444
2445                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
2446
2447        return view
2448
2449    def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True, onlyFiles=False) -> tuple[list[dict], dict]:
2450        """
2451        Returns history operations between two given dates for current `accountId`.
2452        If `reportFile` string is not empty then also save human-readable report.
2453        Shows some statistical data of closed positions.
2454
2455        :param start: see docstring in `TradeRoutines.GetDatesAsString()` method.
2456        :param end: see docstring in `TradeRoutines.GetDatesAsString()` method.
2457        :param show: if `True` then also prints all records to the console.
2458        :param showCancelled: if `False` then remove information about cancelled operations from the deals report.
2459        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
2460        :return: original list of dictionaries with history of deals records from API ("operations" key):
2461                 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2462                 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc.
2463        """
2464        if self.accountId is None or not self.accountId:
2465            uLogger.error("Variable `accountId` must be defined for using this method!")
2466            raise Exception("Account ID required")
2467
2468        startDate, endDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT)  # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2469
2470        uLogger.debug("Requesting history of a client's operations. Wait, please...")
2471
2472        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2473        dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations"
2474        self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate})
2475        ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"]  # list of dict: operations returns by broker
2476        customStat = {}  # custom statistics in additional to responseJSON
2477
2478        # --- output report in human-readable format:
2479        if self.reportFile and (show or onlyFiles):
2480            splitLine1 = "|                            |                               |                              |                      |                        |\n"  # Summary section
2481            splitLine2 = "|                     |              |              |            |           |                 |            |                                                                    |\n"  # Operations section
2482            nextDay = ""
2483
2484            info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])]
2485
2486            if len(ops) > 0:
2487                customStat = {
2488                    "opsCount": 0,  # total operations count
2489                    "buyCount": 0,  # buy operations
2490                    "sellCount": 0,  # sell operations
2491                    "buyTotal": {"rub": 0.},  # Buy sums in different currencies
2492                    "sellTotal": {"rub": 0.},  # Sell sums in different currencies
2493                    "payIn": {"rub": 0.},  # Deposit brokerage account
2494                    "payOut": {"rub": 0.},  # Withdrawals
2495                    "divs": {"rub": 0.},  # Dividends income
2496                    "coupons": {"rub": 0.},  # Coupon's income
2497                    "brokerCom": {"rub": 0.},  # Service commissions
2498                    "serviceCom": {"rub": 0.},  # Service commissions
2499                    "marginCom": {"rub": 0.},  # Margin commissions
2500                    "allTaxes": {"rub": 0.},  # Sum of withholding taxes and corrections
2501                }
2502
2503                # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES:
2504                for item in ops:
2505                    if item["state"] == "OPERATION_STATE_EXECUTED":
2506                        payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2507
2508                        # count buy operations:
2509                        if "_BUY" in item["operationType"]:
2510                            customStat["buyCount"] += 1
2511
2512                            if item["payment"]["currency"] in customStat["buyTotal"].keys():
2513                                customStat["buyTotal"][item["payment"]["currency"]] += payment
2514
2515                            else:
2516                                customStat["buyTotal"][item["payment"]["currency"]] = payment
2517
2518                        # count sell operations:
2519                        elif "_SELL" in item["operationType"]:
2520                            customStat["sellCount"] += 1
2521
2522                            if item["payment"]["currency"] in customStat["sellTotal"].keys():
2523                                customStat["sellTotal"][item["payment"]["currency"]] += payment
2524
2525                            else:
2526                                customStat["sellTotal"][item["payment"]["currency"]] = payment
2527
2528                        # count incoming operations:
2529                        elif item["operationType"] in ["OPERATION_TYPE_INPUT"]:
2530                            if item["payment"]["currency"] in customStat["payIn"].keys():
2531                                customStat["payIn"][item["payment"]["currency"]] += payment
2532
2533                            else:
2534                                customStat["payIn"][item["payment"]["currency"]] = payment
2535
2536                        # count withdrawals operations:
2537                        elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]:
2538                            if item["payment"]["currency"] in customStat["payOut"].keys():
2539                                customStat["payOut"][item["payment"]["currency"]] += payment
2540
2541                            else:
2542                                customStat["payOut"][item["payment"]["currency"]] = payment
2543
2544                        # count dividends income:
2545                        elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]:
2546                            if item["payment"]["currency"] in customStat["divs"].keys():
2547                                customStat["divs"][item["payment"]["currency"]] += payment
2548
2549                            else:
2550                                customStat["divs"][item["payment"]["currency"]] = payment
2551
2552                        # count coupon's income:
2553                        elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]:
2554                            if item["payment"]["currency"] in customStat["coupons"].keys():
2555                                customStat["coupons"][item["payment"]["currency"]] += payment
2556
2557                            else:
2558                                customStat["coupons"][item["payment"]["currency"]] = payment
2559
2560                        # count broker commissions:
2561                        elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]:
2562                            if item["payment"]["currency"] in customStat["brokerCom"].keys():
2563                                customStat["brokerCom"][item["payment"]["currency"]] += payment
2564
2565                            else:
2566                                customStat["brokerCom"][item["payment"]["currency"]] = payment
2567
2568                        # count service commissions:
2569                        elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]:
2570                            if item["payment"]["currency"] in customStat["serviceCom"].keys():
2571                                customStat["serviceCom"][item["payment"]["currency"]] += payment
2572
2573                            else:
2574                                customStat["serviceCom"][item["payment"]["currency"]] = payment
2575
2576                        # count margin commissions:
2577                        elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]:
2578                            if item["payment"]["currency"] in customStat["marginCom"].keys():
2579                                customStat["marginCom"][item["payment"]["currency"]] += payment
2580
2581                            else:
2582                                customStat["marginCom"][item["payment"]["currency"]] = payment
2583
2584                        # count withholding taxes:
2585                        elif "_TAX" in item["operationType"]:
2586                            if item["payment"]["currency"] in customStat["allTaxes"].keys():
2587                                customStat["allTaxes"][item["payment"]["currency"]] += payment
2588
2589                            else:
2590                                customStat["allTaxes"][item["payment"]["currency"]] = payment
2591
2592                        else:
2593                            continue
2594
2595                customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"]
2596
2597                # --- view "Actions" lines:
2598                info.extend([
2599                    "| Report sections            |                               |                              |                      |                        |\n",
2600                    "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n",
2601                    "| **Actions:**               | Trades: {:<21} | Trading volumes:             |                      |                        |\n".format(customStat["opsCount"]),
2602                    "|                            |   Buy: {:<22} | {:<28} |                      |                        |\n".format(
2603                        "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2604                        "  rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else "  —",
2605                    ),
2606                    "|                            |   Sell: {:<21} | {:<28} |                      |                        |\n".format(
2607                        "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2608                        "  rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else "  —",
2609                    ),
2610                ])
2611
2612                opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys()))))
2613                for key in opsKeys:
2614                    if key == "rub":
2615                        continue
2616
2617                    info.extend([
2618                        "|                            |                               | {:<28} |                      |                        |\n".format(
2619                            "  {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0)
2620                        ),
2621                        "|                            |                               | {:<28} |                      |                        |\n".format(
2622                            "  {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0)
2623                        ),
2624                    ])
2625
2626                info.append(splitLine1)
2627
2628                def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str:
2629                    return "|                            | {:<29} | {:<28} | {:<20} | {:<22} |\n".format(
2630                            "  {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else "  —",
2631                            "  {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else "  —",
2632                            "  {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else "  —",
2633                            "  {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else "  —",
2634                    )
2635
2636                # --- view "Payments" lines:
2637                info.append("| **Payments:**              | Deposit on broker account:    | Withdrawals:                 | Dividends income:    | Coupons income:        |\n")
2638                paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys()))))
2639
2640                for key in paymentsKeys:
2641                    info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key))
2642
2643                info.append(splitLine1)
2644
2645                # --- view "Commissions and taxes" lines:
2646                info.append("| **Commissions and taxes:** | Broker commissions:           | Service commissions:         | Margin commissions:  | All taxes/corrections: |\n")
2647                comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys()))))
2648
2649                for key in comKeys:
2650                    info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key))
2651
2652                info.extend([
2653                    "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"),
2654                    "| Date and time       | FIGI         | Ticker       | Asset      | Value     | Payment         | Status     | Operation type                                                     |\n",
2655                    "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n",
2656                ])
2657
2658            else:
2659                info.append("Broker returned no operations during this period\n")
2660
2661            # --- view "Operations" section:
2662            for item in ops:
2663                if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]:
2664                    continue
2665
2666                else:
2667                    self._figi = item["figi"]
2668                    payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2669                    instrument = self.SearchByFIGI(requestPrice=False) if self._figi else {}
2670
2671                    # group of deals during one day:
2672                    if nextDay and item["date"].split("T")[0] != nextDay:
2673                        info.append(splitLine2)
2674                        nextDay = ""
2675
2676                    else:
2677                        nextDay = item["date"].split("T")[0]  # saving current day for splitting
2678
2679                    info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format(
2680                        item["date"].replace("T", " ").replace("Z", "").split(".")[0],
2681                        self._figi if self._figi else "—",
2682                        instrument["ticker"] if instrument else "—",
2683                        instrument["type"] if instrument else "—",
2684                        item["quantity"] if int(item["quantity"]) > 0 else "—",
2685                        "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—",
2686                        TKS_OPERATION_STATES[item["state"]],
2687                        TKS_OPERATION_TYPES[item["operationType"]],
2688                    ))
2689
2690            infoText = "".join(info)
2691
2692            if show and not onlyFiles:
2693                if self.moreDebug:
2694                    uLogger.debug("Records about history of a client's operations successfully received")
2695
2696                uLogger.info(infoText)
2697
2698            if self.reportFile and (show or onlyFiles):
2699                with open(self.reportFile, "w", encoding="UTF-8") as fH:
2700                    fH.write(infoText)
2701
2702                uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile)))
2703
2704                if self.useHTMLReports:
2705                    htmlFilePath = self.reportFile.replace(".md", ".html") if self.reportFile.endswith(".md") else self.reportFile + ".html"
2706                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
2707                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Client's operations", commonCSS=COMMON_CSS, markdown=infoText))
2708
2709                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
2710
2711        return ops, customStat
2712
2713    def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False, onlyFiles=False) -> pd.DataFrame:
2714        """
2715        This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id).
2716
2717        History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`.
2718        Warning! Broker server used ISO UTC time by default.
2719
2720        If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame.
2721        Also, `historyFile` used to update history with `onlyMissing` parameter.
2722
2723        See also: `LoadHistory()` and `ShowHistoryChart()` methods.
2724
2725        :param start: see docstring in `TradeRoutines.GetDatesAsString()` method.
2726        :param end: see docstring in `TradeRoutines.GetDatesAsString()` method.
2727        :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`,
2728                         `"hour"`, `"day"`. Default: `"hour"`.
2729        :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`.
2730                            False by default. Warning! History appends only from last candle to current time
2731                            with always update last candle!
2732        :param csvSep: separator if csv-file is used, `,` by default.
2733        :param show: if `True` then also prints Pandas DataFrame to the console.
2734        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
2735        :return: Pandas DataFrame with prices history. Headers of columns are defined by default:
2736                 `["date", "time", "open", "high", "low", "close", "volume"]`.
2737        """
2738        strStartDate, strEndDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT)  # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2739        headers = ["date", "time", "open", "high", "low", "close", "volume"]  # sequence and names of column headers
2740        history = None  # empty pandas object for history
2741
2742        if interval not in TKS_CANDLE_INTERVALS.keys():
2743            uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.")
2744            raise Exception("Incorrect value")
2745
2746        if not (self._ticker or self._figi):
2747            uLogger.error("Ticker or FIGI must be defined!")
2748            raise Exception("Ticker or FIGI required")
2749
2750        if self._ticker and not self._figi:
2751            instrumentByTicker = self.SearchByTicker(requestPrice=False)
2752            self._figi = instrumentByTicker["figi"] if instrumentByTicker else ""
2753
2754        if self._figi and not self._ticker:
2755            instrumentByFIGI = self.SearchByFIGI(requestPrice=False)
2756            self._ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else ""
2757
2758        dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from start time string
2759        dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from end time string
2760        if interval.lower() != "day":
2761            dtEnd += timedelta(seconds=1)  # adds 1 sec for requests, because day end returned by `TradeRoutines.GetDatesAsString()` is 23:59:59
2762
2763        delta = dtEnd - dtStart  # current UTC time minus last time in file
2764        deltaMinutes = delta.days * 1440 + delta.seconds // 60  # minutes between start and end dates
2765
2766        # calculate history length in candles:
2767        length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1]
2768        if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0:
2769            length += 1  # to avoid fraction time
2770
2771        # calculate data blocks count:
2772        blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2]
2773
2774        uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self._ticker, self._figi))
2775        if self.moreDebug:
2776            uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end))
2777            uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate))
2778            uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval))
2779            uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2]))
2780
2781        tempOld = None  # pandas object for old history, if --only-missing key present
2782        lastTime = None  # datetime object of last old candle in file
2783
2784        if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile):
2785            if self.moreDebug:
2786                uLogger.debug("--only-missing key present, add only last missing candles...")
2787                uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile)))
2788
2789            tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers)
2790
2791            tempOld["date"] = pd.to_datetime(tempOld["date"])  # load date "as is"
2792            tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d")  # convert date to string
2793            tempOld["time"] = pd.to_datetime(tempOld["time"])  # load time "as is"
2794            tempOld["time"] = tempOld["time"].dt.strftime("%H:%M")  # convert time to string
2795
2796            # get last datetime object from last string in file or minus 1 delta if file is empty:
2797            if len(tempOld) > 0:
2798                lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2799
2800            else:
2801                lastTime = dtEnd - timedelta(days=1)  # history file is empty, so last date set at -1 day
2802
2803            tempOld = tempOld[:-1]  # always remove last old candle because it may be incompletely at the current time
2804
2805        responseJSONs = []  # raw history blocks of data
2806
2807        blockEnd = dtEnd
2808        for item in range(blocks):
2809            tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2]
2810            blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail)
2811
2812            uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format(
2813                item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2814            ))
2815
2816            if blockStart == blockEnd:
2817                uLogger.debug("Skipped this zero-length block...")
2818
2819            else:
2820                # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles
2821                historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles"
2822                self.body = str({
2823                    "figi": self._figi,
2824                    "from": blockStart.strftime(TKS_DATE_TIME_FORMAT),
2825                    "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2826                    "interval": TKS_CANDLE_INTERVALS[interval][0]
2827                })
2828                responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1)
2829
2830                if "code" in responseJSON.keys():
2831                    uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks))
2832
2833                else:
2834                    if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1:
2835                        responseJSON["candles"] = responseJSON["candles"][:-1]  # removes last candle for "yesterday" request
2836
2837                    responseJSONs = responseJSON["candles"] + responseJSONs  # add more old history behind newest dates
2838
2839            blockEnd = blockStart
2840
2841        printCount = len(responseJSONs)  # candles to show in console
2842        if responseJSONs:
2843            tempHistory = pd.DataFrame(
2844                data={
2845                    "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2846                    "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2847                    "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs],
2848                    "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs],
2849                    "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs],
2850                    "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs],
2851                    "volume": [int(item["volume"]) for item in responseJSONs],
2852                },
2853                index=range(len(responseJSONs)),
2854                columns=["date", "time", "open", "high", "low", "close", "volume"],
2855            )
2856            tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d")
2857            tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M")
2858
2859            # append only newest candles to old history if --only-missing key present:
2860            if onlyMissing and tempOld is not None and lastTime is not None:
2861                index = 0  # find start index in tempHistory data:
2862
2863                for i, item in tempHistory.iterrows():
2864                    curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2865
2866                    if curTime == lastTime:
2867                        uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
2868                        index = i
2869                        printCount = index + 1
2870                        break
2871
2872                history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True)
2873
2874            else:
2875                history = tempHistory  # if no `--only-missing` key then load full data from server
2876
2877            if self.moreDebug:
2878                uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False)))
2879
2880        if history is not None and not history.empty:
2881            if show and not onlyFiles:
2882                uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format(
2883                    strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]),
2884                    pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False),
2885                ))
2886
2887        else:
2888            uLogger.warning("Received an empty candles history!")
2889
2890        if self.historyFile is not None:
2891            if history is not None and not history.empty:
2892                history.to_csv(self.historyFile, sep=csvSep, index=False, header=False)
2893                uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self._ticker, self._figi, interval, os.path.abspath(self.historyFile)))
2894
2895            else:
2896                uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile)))
2897
2898        else:
2899            if self.moreDebug:
2900                uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.")
2901
2902        return history
2903
2904    def LoadHistory(self, filePath: str) -> pd.DataFrame:
2905        """
2906        Load candles history from csv-file and return Pandas DataFrame object.
2907
2908        See also: `History()` and `ShowHistoryChart()` methods.
2909
2910        :param filePath: path to csv-file to open.
2911        """
2912        loadedHistory = None  # init candles data object
2913
2914        uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...")
2915
2916        if os.path.exists(filePath):
2917            loadedHistory = self.priceModel.LoadFromFile(filePath)  # load data and get chain of candles as Pandas DataFrame
2918
2919            tfStr = self.priceModel.FormattedDelta(
2920                self.priceModel.timeframe,
2921                "{days} days {hours}h {minutes}m {seconds}s",
2922            ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta(
2923                self.priceModel.timeframe,
2924                "{hours}h {minutes}m {seconds}s",
2925            )
2926
2927            if loadedHistory is not None and not loadedHistory.empty:
2928                uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format(
2929                    len(loadedHistory),
2930                    tfStr,
2931                    pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)),
2932                )
2933
2934            else:
2935                uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath)))
2936
2937        else:
2938            uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath))
2939
2940        return loadedHistory
2941
2942    def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None:
2943        """
2944        Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file.
2945
2946        Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart.
2947        Default: `index.html` (both for interact and non-interact candlesticks chart).
2948
2949        See also: `History()` and `LoadHistory()` methods.
2950
2951        :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object.
2952        :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart.
2953                         See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters
2954                         If False then chain of candlesticks will render as not interactive Google Candlestick chart.
2955                         See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template
2956        :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to
2957                              html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file.
2958        """
2959        if isinstance(candles, str):
2960            self.priceModel.prices = self.LoadHistory(filePath=candles)  # load candles chain from file
2961            self.priceModel.ticker = os.path.basename(candles)  # use filename as ticker name in PriceGenerator
2962
2963        elif isinstance(candles, pd.DataFrame):
2964            self.priceModel.prices = candles  # set candles chain from variable
2965            self.priceModel.ticker = self._ticker  # use current TKSBrokerAPI ticker as ticker name in PriceGenerator
2966
2967            if "datetime" not in candles.columns:
2968                self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True)  # PriceGenerator uses "datetime" column with date and time
2969
2970        else:
2971            uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!")
2972            raise Exception("Incorrect value")
2973
2974        self.priceModel.horizon = len(self.priceModel.prices)  # use length of candles data as horizon in PriceGenerator
2975
2976        if interact:
2977            uLogger.debug("Rendering interactive candles chart. Wait, please...")
2978
2979            self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2980
2981        else:
2982            uLogger.debug("Rendering non-interactive candles chart. Wait, please...")
2983
2984            self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2985
2986        uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile)))
2987
2988    def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2989        """
2990        Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response.
2991        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
2992
2993        See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`.
2994
2995        :param operation: string "Buy" or "Sell".
2996        :param lots: volume, integer count of lots >= 1.
2997        :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`.
2998        :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`.
2999        :param expDate: string "Undefined" by default or local date in future,
3000                        it is a string with format `%Y-%m-%d %H:%M:%S`.
3001        :return: JSON with response from broker server.
3002        """
3003        if self.accountId is None or not self.accountId:
3004            uLogger.error("Variable `accountId` must be defined for using this method!")
3005            raise Exception("Account ID required")
3006
3007        if operation is None or not operation or operation not in ("Buy", "Sell"):
3008            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
3009            raise Exception("Incorrect value")
3010
3011        if lots is None or lots < 1:
3012            uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.")
3013            lots = 1
3014
3015        if tp is None or tp < 0:
3016            tp = 0
3017
3018        if sl is None or sl < 0:
3019            sl = 0
3020
3021        if expDate is None or not expDate:
3022            expDate = "Undefined"
3023
3024        if not (self._ticker or self._figi):
3025            uLogger.error("Ticker or FIGI must be defined!")
3026            raise Exception("Ticker or FIGI required")
3027
3028        instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True)
3029        self._ticker = instrument["ticker"]
3030        self._figi = instrument["figi"]
3031
3032        uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self._ticker, self._figi, lots, tp, sl, expDate))
3033
3034        openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
3035        self.body = str({
3036            "figi": self._figi,
3037            "quantity": str(lots),
3038            "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
3039            "accountId": str(self.accountId),
3040            "orderType": "ORDER_TYPE_MARKET",  # see: TKS_ORDER_TYPES
3041        })
3042        response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0)
3043
3044        if "orderId" in response.keys():
3045            uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format(
3046                operation, response["orderId"],
3047                self._ticker, self._figi, lots,
3048                NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"],
3049                NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"],
3050                NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"],
3051            ))
3052
3053            if tp > 0:
3054                self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate)
3055
3056            if sl > 0:
3057                self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate)
3058
3059        else:
3060            uLogger.warning("Not `oK` status received! Market order not executed. See full debug log and try again open order later.")
3061
3062        return response
3063
3064    def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
3065        """
3066        More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response.
3067        If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter.
3068
3069        See also: `Order()` and `Trade()` docstrings.
3070
3071        :param lots: volume, integer count of lots >= 1.
3072        :param tp: float > 0, take profit price of stop-order.
3073        :param sl: float > 0, stop loss price of stop-order.
3074        :param expDate: it's a local date in future.
3075                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3076        :return: JSON with response from broker server.
3077        """
3078        return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate)
3079
3080    def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
3081        """
3082        More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response.
3083        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
3084
3085        See also: `Order()` and `Trade()` docstrings.
3086
3087        :param lots: volume, integer count of lots >= 1.
3088        :param tp: float > 0, take profit price of stop-order.
3089        :param sl: float > 0, stop loss price of stop-order.
3090        :param expDate: it's a local date in the future.
3091                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3092        :return: JSON with response from broker server.
3093        """
3094        return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate)
3095
3096    def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None:
3097        """
3098        Close position of given instruments.
3099
3100        :param instruments: list of instruments defined by tickers or FIGIs that must be closed.
3101        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
3102                         This avoids unnecessary downloading data from the server.
3103        """
3104        if instruments is None or not instruments:
3105            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
3106            raise Exception("Ticker or FIGI required")
3107
3108        if isinstance(instruments, str):
3109            instruments = [instruments]
3110
3111        uniqueInstruments = self.GetUniqueFIGIs(instruments)
3112        if uniqueInstruments:
3113            if portfolio is None or not portfolio:
3114                portfolio = self.Overview(show=False)
3115
3116            allOpened = [item["figi"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]]
3117            uLogger.debug("All opened instruments by it's FIGI: {}".format(", ".join(allOpened)))
3118
3119            for self._figi in uniqueInstruments:
3120                if self._figi not in allOpened:
3121                    uLogger.warning("Instrument with FIGI [{}] not in open positions list!".format(self._figi))
3122                    continue
3123
3124                # search open trade info about instrument by ticker:
3125                instrument = {}
3126                for iType in TKS_INSTRUMENTS:
3127                    if instrument:
3128                        break
3129
3130                    for item in portfolio["stat"][iType]:
3131                        if item["figi"] == self._figi:
3132                            instrument = item
3133                            break
3134
3135                if instrument:
3136                    self._ticker = instrument["ticker"]
3137                    self._figi = instrument["figi"]
3138
3139                    uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format(
3140                        self._ticker,
3141                        self._figi,
3142                        int(instrument["volume"]),
3143                        ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "",
3144                    ))
3145
3146                    tradeLots = abs(instrument["lots"]) - instrument["blocked"]  # available volumes in lots for close operation
3147
3148                    if tradeLots > 0:
3149                        if instrument["blocked"] > 0:
3150                            uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format(
3151                                instrument["blocked"],
3152                                self._ticker,
3153                                tradeLots,
3154                            ))
3155
3156                        # if direction is "Long" then we need sell, if direction is "Short" then we need buy:
3157                        self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots)
3158
3159                    else:
3160                        uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self._ticker))
3161
3162    def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None:
3163        """
3164        Close all positions of given instruments with defined type.
3165
3166        :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list.
3167        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
3168                         This avoids unnecessary downloading data from the server.
3169        """
3170        if iType not in TKS_INSTRUMENTS:
3171            uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType))
3172
3173        else:
3174            if portfolio is None or not portfolio:
3175                portfolio = self.Overview(show=False)
3176
3177            tickers = [item["ticker"] for item in portfolio["stat"][iType]]
3178            uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers))
3179
3180            if tickers and portfolio:
3181                self.CloseTrades(tickers, portfolio)
3182
3183            else:
3184                uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType))
3185
3186    def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3187        """
3188        Universal method to create market or limit orders with all available parameters for current `accountId`.
3189        See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`.
3190
3191        If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above
3192        current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day.
3193
3194        Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell"
3195        then broker immediately open market order as you can do simple --buy or --sell operations!
3196
3197        If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell".
3198        When current price will go up or down to target price value then broker opens a limit order.
3199        Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter.
3200
3201        Only one attempt and no retry for opens order. If network issue occurred you can create new request.
3202
3203        :param operation: string "Buy" or "Sell".
3204        :param orderType: string "Limit" or "Stop".
3205        :param lots: volume, integer count of lots >= 1.
3206        :param targetPrice: target price > 0. This is open trade price for limit order.
3207        :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice.
3208                           Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order.
3209        :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types
3210                         "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3211                         Stop loss order always executed by market price.
3212        :param expDate: string "Undefined" by default or local date in future.
3213                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3214                        This date is converting to UTC format for server. This parameter only makes sense for stop-order.
3215                        A limit order has no expiration date, it lasts until the end of the trading day.
3216        :return: JSON with response from broker server.
3217        """
3218        if self.accountId is None or not self.accountId:
3219            uLogger.error("Variable `accountId` must be defined for using this method!")
3220            raise Exception("Account ID required")
3221
3222        if operation is None or not operation or operation not in ("Buy", "Sell"):
3223            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
3224            raise Exception("Incorrect value")
3225
3226        if orderType is None or not orderType or orderType not in ("Limit", "Stop"):
3227            uLogger.error("You must define order type only one of them: `Limit` or `Stop`!")
3228            raise Exception("Incorrect value")
3229
3230        if lots is None or lots < 1:
3231            uLogger.error("You must define trade volume > 0: integer count of lots!")
3232            raise Exception("Incorrect value")
3233
3234        if targetPrice is None or targetPrice <= 0:
3235            uLogger.error("Target price for limit-order must be greater than 0!")
3236            raise Exception("Incorrect value")
3237
3238        if limitPrice is None or limitPrice <= 0:
3239            limitPrice = targetPrice
3240
3241        if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"):
3242            stopType = "Limit"
3243
3244        if expDate is None or not expDate:
3245            expDate = "Undefined"
3246
3247        if not (self._ticker or self._figi):
3248            uLogger.error("Tocker or FIGI must be defined!")
3249            raise Exception("Ticker or FIGI required")
3250
3251        response = {}
3252        instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True)
3253        self._ticker = instrument["ticker"]
3254        self._figi = instrument["figi"]
3255
3256        if orderType == "Limit":
3257            uLogger.debug(
3258                "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format(
3259                    self._ticker, self._figi,
3260                    operation, lots, targetPrice, instrument["currency"],
3261                ))
3262
3263            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
3264            self.body = str({
3265                "figi": self._figi,
3266                "quantity": str(lots),
3267                "price": FloatToNano(targetPrice),
3268                "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
3269                "accountId": str(self.accountId),
3270                "orderType": "ORDER_TYPE_LIMIT",  # see: TKS_ORDER_TYPES
3271            })
3272            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0)
3273
3274            if "orderId" in response.keys():
3275                uLogger.info(
3276                    "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{} {}]".format(
3277                        response["orderId"], self._ticker, self._figi, operation, lots,
3278                        "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"],
3279                    ))
3280
3281                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3282                    if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]:
3283                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format(
3284                            targetPrice, instrument["currency"],
3285                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3286                        ))
3287
3288                    if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]:
3289                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format(
3290                            targetPrice, instrument["currency"],
3291                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3292                        ))
3293
3294            else:
3295                uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log and try again open order later.")
3296
3297        if orderType == "Stop":
3298            uLogger.debug(
3299                "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format(
3300                    self._ticker, self._figi,
3301                    operation, lots,
3302                    targetPrice, instrument["currency"],
3303                    limitPrice, instrument["currency"],
3304                    stopType, expDate,
3305                ))
3306
3307            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder"
3308            expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT)
3309            stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT"
3310
3311            body = {
3312                "figi": self._figi,
3313                "quantity": str(lots),
3314                "price": FloatToNano(limitPrice),
3315                "stopPrice": FloatToNano(targetPrice),
3316                "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL",  # see: TKS_STOP_ORDER_DIRECTIONS
3317                "accountId": str(self.accountId),
3318                "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL",  # see: TKS_STOP_ORDER_EXPIRATION_TYPES
3319                "stopOrderType": stopOrderType,  # see: TKS_STOP_ORDER_TYPES
3320            }
3321
3322            if expDateUTC:
3323                body["expireDate"] = expDateUTC
3324
3325            self.body = str(body)
3326            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0)
3327
3328            if "stopOrderId" in response.keys():
3329                uLogger.info(
3330                    "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{} {}], limit price [{} {}], stop-order type [{}] and expiration date [{} UTC]".format(
3331                        response["stopOrderId"], self._ticker, self._figi, operation, lots,
3332                        "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"],
3333                        "{:.4f}".format(limitPrice).rstrip("0").rstrip("."), instrument["currency"],
3334                        TKS_STOP_ORDER_TYPES[stopOrderType],
3335                        datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"],
3336                    ))
3337
3338                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3339                    if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3340                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3341                            targetPrice, instrument["currency"],
3342                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3343                        ))
3344
3345                    if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3346                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3347                            targetPrice, instrument["currency"],
3348                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3349                        ))
3350
3351            else:
3352                uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log and try again open order later.")
3353
3354        return response
3355
3356    def BuyLimit(self, lots: int, targetPrice: float) -> dict:
3357        """
3358        Create pending `Buy` limit-order (below current price). You must specify only 2 parameters:
3359        `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then
3360        broker immediately open `Buy` market order, such as if you do simple `--buy` operation!
3361        See also: `Order()` docstring.
3362
3363        :param lots: volume, integer count of lots >= 1.
3364        :param targetPrice: target price > 0. This is open trade price for limit order.
3365        :return: JSON with response from broker server.
3366        """
3367        return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice)
3368
3369    def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3370        """
3371        Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order.
3372        In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3373        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3374        target price value then broker opens a limit order. See also: `Order()` docstring.
3375
3376        :param lots: volume, integer count of lots >= 1.
3377        :param targetPrice: target price > 0. This is trigger price for buy stop-order.
3378        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3379                           with price equal to limitPrice, when current price goes to target price of buy stop-order.
3380        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3381                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3382        :param expDate: string "Undefined" by default or local date in future.
3383                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3384                        This date is converting to UTC format for server.
3385        :return: JSON with response from broker server.
3386        """
3387        return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
3388
3389    def SellLimit(self, lots: int, targetPrice: float) -> dict:
3390        """
3391        Create pending `Sell` limit-order (above current price). You must specify only 2 parameters:
3392        `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then
3393        broker immediately open `Sell` market order, such as if you do simple `--sell` operation!
3394        See also: `Order()` docstring.
3395
3396        :param lots: volume, integer count of lots >= 1.
3397        :param targetPrice: target price > 0. This is open trade price for limit order.
3398        :return: JSON with response from broker server.
3399        """
3400        return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice)
3401
3402    def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3403        """
3404        Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order.
3405        In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3406        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3407        target price value then broker opens a limit order. See also: `Order()` docstring.
3408
3409        :param lots: volume, integer count of lots >= 1.
3410        :param targetPrice: target price > 0. This is trigger price for sell stop-order.
3411        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3412                           with price equal to limitPrice, when current price goes to target price of sell stop-order.
3413        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3414                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3415        :param expDate: string "Undefined" by default or local date in future.
3416                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3417                        This date is converting to UTC format for server.
3418        :return: JSON with response from broker server.
3419        """
3420        return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
3421
3422    def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None:
3423        """
3424        Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`.
3425
3426        :param orderIDs: list of integers with `orderId` or `stopOrderId`.
3427        :param allOrdersIDs: pre-received lists of all active pending limit orders.
3428                             This avoids unnecessary downloading data from the server.
3429        :param allStopOrdersIDs: pre-received lists of all active stop orders.
3430        """
3431        if self.accountId is None or not self.accountId:
3432            uLogger.error("Variable `accountId` must be defined for using this method!")
3433            raise Exception("Account ID required")
3434
3435        if orderIDs:
3436            if allOrdersIDs is None:
3437                rawOrders = self.RequestPendingOrders()
3438                allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending limit orders ID
3439
3440            if allStopOrdersIDs is None:
3441                rawStopOrders = self.RequestStopOrders()
3442                allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3443
3444            for orderID in orderIDs:
3445                idInPendingOrders = orderID in allOrdersIDs
3446                idInStopOrders = orderID in allStopOrdersIDs
3447
3448                if not (idInPendingOrders or idInStopOrders):
3449                    uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID))
3450                    continue
3451
3452                else:
3453                    if idInPendingOrders:
3454                        uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID))
3455
3456                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder
3457                        self.body = str({"accountId": self.accountId, "orderId": orderID})
3458                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder"
3459                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3460
3461                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3462                            if self.moreDebug:
3463                                uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3464
3465                            uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID))
3466
3467                        else:
3468                            uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID))
3469
3470                    elif idInStopOrders:
3471                        uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID))
3472
3473                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder
3474                        self.body = str({"accountId": self.accountId, "stopOrderId": orderID})
3475                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder"
3476                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3477
3478                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3479                            if self.moreDebug:
3480                                uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3481
3482                            uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID))
3483
3484                        else:
3485                            uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID))
3486
3487                    else:
3488                        continue
3489
3490    def CloseAllOrders(self) -> None:
3491        """
3492        Gets a list of open pending and stop orders and cancel it all.
3493        """
3494        rawOrders = self.RequestPendingOrders()
3495        allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending limit orders ID
3496        lenOrders = len(allOrdersIDs)
3497
3498        rawStopOrders = self.RequestStopOrders()
3499        allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3500        lenSOrders = len(allStopOrdersIDs)
3501
3502        if lenOrders > 0 or lenSOrders > 0:
3503            uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders))
3504
3505            self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs)
3506
3507        else:
3508            uLogger.info("Orders not found, nothing to cancel.")
3509
3510    def CloseAll(self, *args) -> None:
3511        """
3512        Close all available (not blocked) opened trades and orders.
3513
3514        Also, you can select one or more keywords case-insensitive:
3515        `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type.
3516
3517        Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods.
3518        """
3519        overview = self.Overview(show=False)  # get all open trades info
3520
3521        if len(args) == 0:
3522            uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...")
3523            self.CloseAllOrders()  # close all pending and stop orders
3524
3525            for iType in TKS_INSTRUMENTS:
3526                if iType != "Currencies":
3527                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies
3528
3529        else:
3530            uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args)))
3531            lowerArgs = [x.lower() for x in args]
3532
3533            if "orders" in lowerArgs:
3534                self.CloseAllOrders()  # close all pending and stop orders
3535
3536            for iType in TKS_INSTRUMENTS:
3537                if iType.lower() in lowerArgs and iType != "Currencies":
3538                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies
3539
3540    def CloseAllByTicker(self, instrument: str) -> None:
3541        """
3542        Close all available (not blocked) opened trades and orders for one instrument defined by its ticker.
3543
3544        This method searches opened trade and orders of instrument throw all portfolio and then use
3545        `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument.
3546
3547        See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`.
3548
3549        :param instrument: string with ticker.
3550        """
3551        if instrument is None or not instrument:
3552            uLogger.error("Ticker name must be defined for using this method!")
3553            raise Exception("Ticker required")
3554
3555        overview = self.Overview(show=False)  # get user portfolio with all open trades info
3556
3557        self._ticker = instrument  # try to set instrument as ticker
3558        self._figi = ""
3559
3560        limitAll = [item["orderID"] for item in overview["stat"]["orders"]]  # list of all pending limit order IDs
3561        stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]]  # list of all stop order IDs
3562
3563        if limitAll and self.IsInLimitOrders(portfolio=overview):
3564            uLogger.debug("Closing all opened pending limit orders for the instrument with ticker [{}]. Wait, please...")
3565            self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3566
3567        if stopAll and self.IsInStopOrders(portfolio=overview):
3568            uLogger.debug("Closing all opened stop orders for the instrument with ticker [{}]. Wait, please...")
3569            self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3570
3571        if self.IsInPortfolio(portfolio=overview):
3572            uLogger.debug("Closing all available (not blocked) opened trade for the instrument with ticker [{}]. Wait, please...")
3573            self.CloseTrades(instruments=[instrument], portfolio=overview)
3574
3575    def CloseAllByFIGI(self, instrument: str) -> None:
3576        """
3577        Close all available (not blocked) opened trades and orders for one instrument defined by its FIGI id.
3578
3579        This method searches opened trade and orders of instrument throw all portfolio and then use
3580        `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument.
3581
3582        See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`.
3583
3584        :param instrument: string with FIGI id.
3585        """
3586        if instrument is None or not instrument:
3587            uLogger.error("FIGI id must be defined for using this method!")
3588            raise Exception("FIGI required")
3589
3590        overview = self.Overview(show=False)  # get user portfolio with all open trades info
3591
3592        self._ticker = ""
3593        self._figi = instrument  # try to set instrument as FIGI id
3594
3595        limitAll = [item["orderID"] for item in overview["stat"]["orders"]]  # list of all pending limit order IDs
3596        stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]]  # list of all stop order IDs
3597
3598        if limitAll and self.IsInLimitOrders(portfolio=overview):
3599            uLogger.debug("Closing all opened pending limit orders for the instrument with FIGI [{}]. Wait, please...")
3600            self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3601
3602        if stopAll and self.IsInStopOrders(portfolio=overview):
3603            uLogger.debug("Closing all opened stop orders for the instrument with FIGI [{}]. Wait, please...")
3604            self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3605
3606        if self.IsInPortfolio(portfolio=overview):
3607            uLogger.debug("Closing all available (not blocked) opened trade for the instrument with FIGI [{}]. Wait, please...")
3608            self.CloseTrades(instruments=[instrument], portfolio=overview)
3609
3610    @staticmethod
3611    def ParseOrderParameters(operation, **inputParameters):
3612        """
3613        Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders.
3614
3615        :param operation: string "Buy" or "Sell".
3616        :param inputParameters: this is dict of strings that looks like this
3617               `{"lots": "L_int,...", "prices": "P_float,..."}` where
3618               "lots" key: one or more lot values (integer numbers) to open with every limit-order
3619               "prices" key: one or more prices to open limit-orders
3620               Counts of values in lots and prices lists must be equals!
3621        :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]`
3622        """
3623        # TODO: update order grid work with api v2
3624        pass
3625        # uLogger.debug("Input parameters: {}".format(inputParameters))
3626        #
3627        # if operation is None or not operation or operation not in ("Buy", "Sell"):
3628        #     uLogger.error("You must define operation type: 'Buy' or 'Sell'!")
3629        #     raise Exception("Incorrect value")
3630        #
3631        # if "l" in inputParameters.keys():
3632        #     inputParameters["lots"] = inputParameters.pop("l")
3633        #
3634        # if "p" in inputParameters.keys():
3635        #     inputParameters["prices"] = inputParameters.pop("p")
3636        #
3637        # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys():
3638        #     uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!")
3639        #     raise Exception("Incorrect value")
3640        #
3641        # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")]
3642        # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")]
3643        #
3644        # if len(lots) != len(prices):
3645        #     uLogger.error("'lots' and 'prices' lists must have equal length of values!")
3646        #     raise Exception("Incorrect value")
3647        #
3648        # uLogger.debug("Extracted parameters for orders:")
3649        # uLogger.debug("lots = {}".format(lots))
3650        # uLogger.debug("prices = {}".format(prices))
3651        #
3652        # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...]
3653        # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))]
3654        # uLogger.debug("Order parameters: {}".format(result))
3655        #
3656        # return result
3657
3658    def IsInPortfolio(self, portfolio: dict = None) -> bool:
3659        """
3660        Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`.
3661
3662        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3663        :return: `True` if portfolio contains open position with given instrument, `False` otherwise.
3664        """
3665        result = False
3666        msg = "Instrument not defined!"
3667
3668        if portfolio is None or not portfolio:
3669            portfolio = self.Overview(show=False)
3670
3671        if self._ticker:
3672            uLogger.debug("Searching instrument with ticker [{}] throw opened positions list...".format(self._ticker))
3673            msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker)
3674
3675            for iType in TKS_INSTRUMENTS:
3676                for instrument in portfolio["stat"][iType]:
3677                    if instrument["ticker"] == self._ticker:
3678                        result = True
3679                        msg = "Instrument with ticker [{}] is present in open positions".format(self._ticker)
3680                        break
3681
3682        elif self._figi:
3683            uLogger.debug("Searching instrument with FIGI [{}] throw opened positions list...".format(self._figi))
3684            msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi)
3685
3686            for iType in TKS_INSTRUMENTS:
3687                for instrument in portfolio["stat"][iType]:
3688                    if instrument["figi"] == self._figi:
3689                        result = True
3690                        msg = "Instrument with FIGI [{}] is present in open positions".format(self._figi)
3691                        break
3692
3693        else:
3694            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3695
3696        uLogger.debug(msg)
3697
3698        return result
3699
3700    def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict:
3701        """
3702        Returns instrument from the user's portfolio if it presents there.
3703        Instrument must be defined by `ticker` (highly priority) or `figi`.
3704
3705        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3706        :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise.
3707        """
3708        result = None
3709        msg = "Instrument not defined!"
3710
3711        if portfolio is None or not portfolio:
3712            portfolio = self.Overview(show=False)
3713
3714        if self._ticker:
3715            uLogger.debug("Searching instrument with ticker [{}] in opened positions...".format(self._ticker))
3716            msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker)
3717
3718            for iType in TKS_INSTRUMENTS:
3719                for instrument in portfolio["stat"][iType]:
3720                    if instrument["ticker"] == self._ticker:
3721                        result = instrument
3722                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self._ticker, instrument["figi"])
3723                        break
3724
3725        elif self._figi:
3726            uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self._figi))
3727            msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi)
3728
3729            for iType in TKS_INSTRUMENTS:
3730                for instrument in portfolio["stat"][iType]:
3731                    if instrument["figi"] == self._figi:
3732                        result = instrument
3733                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self._figi)
3734                        break
3735
3736        else:
3737            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3738
3739        uLogger.debug(msg)
3740
3741        return result
3742
3743    def IsInLimitOrders(self, portfolio: dict = None) -> bool:
3744        """
3745        Checks if instrument is in the limit orders list. Instrument must be defined by `ticker` (highly priority) or `figi`.
3746
3747        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3748
3749        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3750        :return: `True` if limit orders list contains some limit orders for the instrument, `False` otherwise.
3751        """
3752        result = False
3753        msg = "Instrument not defined!"
3754
3755        if portfolio is None or not portfolio:
3756            portfolio = self.Overview(show=False)
3757
3758        if self._ticker:
3759            uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker))
3760            msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker)
3761
3762            for instrument in portfolio["stat"]["orders"]:
3763                if instrument["ticker"] == self._ticker:
3764                    result = True
3765                    msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker)
3766                    break
3767
3768        elif self._figi:
3769            uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi))
3770            msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi)
3771
3772            for instrument in portfolio["stat"]["orders"]:
3773                if instrument["figi"] == self._figi:
3774                    result = True
3775                    msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi)
3776                    break
3777
3778        else:
3779            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3780
3781        uLogger.debug(msg)
3782
3783        return result
3784
3785    def GetLimitOrderIDs(self, portfolio: dict = None) -> list[str]:
3786        """
3787        Returns list with all `orderID`s of opened pending limit orders for the instrument.
3788        Instrument must be defined by `ticker` (highly priority) or `figi`.
3789
3790        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3791
3792        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3793        :return: list with `orderID`s of limit orders.
3794        """
3795        result = []
3796        msg = "Instrument not defined!"
3797
3798        if portfolio is None or not portfolio:
3799            portfolio = self.Overview(show=False)
3800
3801        if self._ticker:
3802            uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker))
3803            msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker)
3804
3805            for instrument in portfolio["stat"]["orders"]:
3806                if instrument["ticker"] == self._ticker:
3807                    result.append(instrument["orderID"])
3808
3809            if result:
3810                msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker)
3811
3812        elif self._figi:
3813            uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi))
3814            msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi)
3815
3816            for instrument in portfolio["stat"]["orders"]:
3817                if instrument["figi"] == self._figi:
3818                    result.append(instrument["orderID"])
3819
3820            if result:
3821                msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi)
3822
3823        else:
3824            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3825
3826        uLogger.debug(msg)
3827
3828        return result
3829
3830    def IsInStopOrders(self, portfolio: dict = None) -> bool:
3831        """
3832        Checks if instrument is in the stop orders list. Instrument must be defined by `ticker` (highly priority) or `figi`.
3833
3834        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3835
3836        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3837        :return: `True` if stop orders list contains some stop orders for the instrument, `False` otherwise.
3838        """
3839        result = False
3840        msg = "Instrument not defined!"
3841
3842        if portfolio is None or not portfolio:
3843            portfolio = self.Overview(show=False)
3844
3845        if self._ticker:
3846            uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker))
3847            msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker)
3848
3849            for instrument in portfolio["stat"]["stopOrders"]:
3850                if instrument["ticker"] == self._ticker:
3851                    result = True
3852                    msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker)
3853                    break
3854
3855        elif self._figi:
3856            uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi))
3857            msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi)
3858
3859            for instrument in portfolio["stat"]["stopOrders"]:
3860                if instrument["figi"] == self._figi:
3861                    result = True
3862                    msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi)
3863                    break
3864
3865        else:
3866            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3867
3868        uLogger.debug(msg)
3869
3870        return result
3871
3872    def GetStopOrderIDs(self, portfolio: dict = None) -> list[str]:
3873        """
3874        Returns list with all `orderID`s of opened stop orders for the instrument.
3875        Instrument must be defined by `ticker` (highly priority) or `figi`.
3876
3877        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3878
3879        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3880        :return: list with `orderID`s of stop orders.
3881        """
3882        result = []
3883        msg = "Instrument not defined!"
3884
3885        if portfolio is None or not portfolio:
3886            portfolio = self.Overview(show=False)
3887
3888        if self._ticker:
3889            uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker))
3890            msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker)
3891
3892            for instrument in portfolio["stat"]["stopOrders"]:
3893                if instrument["ticker"] == self._ticker:
3894                    result.append(instrument["orderID"])
3895
3896            if result:
3897                msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker)
3898
3899        elif self._figi:
3900            uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi))
3901            msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi)
3902
3903            for instrument in portfolio["stat"]["stopOrders"]:
3904                if instrument["figi"] == self._figi:
3905                    result.append(instrument["orderID"])
3906
3907            if result:
3908                msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi)
3909
3910        else:
3911            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3912
3913        uLogger.debug(msg)
3914
3915        return result
3916
3917    def RequestLimits(self) -> dict:
3918        """
3919        Method for obtaining the available funds for withdrawal for current `accountId`.
3920
3921        See also:
3922        - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits
3923        - `OverviewLimits()` method
3924
3925        :return: dict with raw data from server that contains free funds for withdrawal. Example of dict:
3926                 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`.
3927                 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency
3928                 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures.
3929        """
3930        if self.accountId is None or not self.accountId:
3931            uLogger.error("Variable `accountId` must be defined for using this method!")
3932            raise Exception("Account ID required")
3933
3934        uLogger.debug("Requesting current available funds for withdrawal. Wait, please...")
3935
3936        self.body = str({"accountId": self.accountId})
3937        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits"
3938        rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
3939
3940        if self.moreDebug:
3941            uLogger.debug("Records about available funds for withdrawal successfully received")
3942
3943        return rawLimits
3944
3945    def OverviewLimits(self, show: bool = False, onlyFiles=False) -> dict:
3946        """
3947        Method for parsing and show table with available funds for withdrawal for current `accountId`.
3948
3949        See also: `RequestLimits()`.
3950
3951        :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log.
3952        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
3953        :return: dict with raw parsed data from server and some calculated statistics about it.
3954        """
3955        if self.accountId is None or not self.accountId:
3956            uLogger.error("Variable `accountId` must be defined for using this method!")
3957            raise Exception("Account ID required")
3958
3959        rawLimits = self.RequestLimits()  # raw response with current available funds for withdrawal
3960
3961        view = {
3962            "rawLimits": rawLimits,
3963            "limits": {  # parsed data for every currency:
3964                "money": {  # this is an array of portfolio currency positions
3965                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"]
3966                },
3967                "blocked": {  # this is an array of blocked currency
3968                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"]
3969                },
3970                "blockedGuarantee": {  # this is locked money under collateral for futures
3971                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"]
3972                },
3973            },
3974        }
3975
3976        # --- Prepare text table with limits in human-readable format:
3977        if show or onlyFiles:
3978            info = [
3979                "# Withdrawal limits\n\n",
3980                "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
3981                "* **Account ID:** [{}]\n".format(self.accountId),
3982            ]
3983
3984            if view["limits"]["money"]:
3985                info.extend([
3986                    "\n| Currencies | Total         | Available for withdrawal | Blocked for trade | Futures guarantee |\n",
3987                    "|------------|---------------|--------------------------|-------------------|-------------------|\n",
3988                ])
3989
3990            else:
3991                info.append("\nNo withdrawal limits\n")
3992
3993            for curr in view["limits"]["money"].keys():
3994                blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0
3995                blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0
3996                availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee)
3997
3998                infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format(
3999                    "[{}]".format(curr),
4000                    "{:.2f}".format(view["limits"]["money"][curr]),
4001                    "{:.2f}".format(availableMoney),
4002                    "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—",
4003                    "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—",
4004                )
4005
4006                if curr == "rub":
4007                    info.insert(5, infoStr)  # hack: insert "rub" at the first position in table and after headers
4008
4009                else:
4010                    info.append(infoStr)
4011
4012            infoText = "".join(info)
4013
4014            if show and not onlyFiles:
4015                uLogger.info(infoText)
4016
4017            if self.withdrawalLimitsFile and (show or onlyFiles):
4018                with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH:
4019                    fH.write(infoText)
4020
4021                uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile)))
4022
4023                if self.useHTMLReports:
4024                    htmlFilePath = self.withdrawalLimitsFile.replace(".md", ".html") if self.withdrawalLimitsFile.endswith(".md") else self.withdrawalLimitsFile + ".html"
4025                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
4026                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Withdrawal limits", commonCSS=COMMON_CSS, markdown=infoText))
4027
4028                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
4029
4030        return view
4031
4032    def RequestAccounts(self) -> dict:
4033        """
4034        Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`.
4035
4036        See also:
4037        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts
4038        - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account
4039        - `OverviewUserInfo()` method
4040
4041        :return: dict with raw data from server that contains accounts info. Example of dict:
4042                 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account",
4043                   "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z",
4044                   "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`.
4045                 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now.
4046        """
4047        uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...")
4048
4049        self.body = str({})
4050        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts"
4051        rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST")
4052
4053        if self.moreDebug:
4054            uLogger.debug("Records about available accounts successfully received")
4055
4056        return rawAccounts
4057
4058    def RequestUserInfo(self) -> dict:
4059        """
4060        Method for requesting common user's information.
4061
4062        See also:
4063        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo
4064        - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest
4065        - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with
4066        - `OverviewUserInfo()` method
4067
4068        :return: dict with raw data from server that contains user's information. Example of dict:
4069                 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage",
4070                   "russian_shares", "structured_income_bonds"], "tariff": "premium"}`.
4071        """
4072        uLogger.debug("Requesting common user's information. Wait, please...")
4073
4074        self.body = str({})
4075        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo"
4076        rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST")
4077
4078        if self.moreDebug:
4079            uLogger.debug("Records about current user successfully received")
4080
4081        return rawUserInfo
4082
4083    def RequestMarginStatus(self, accountId: str = None) -> dict:
4084        """
4085        Method for requesting margin calculation for defined account ID.
4086
4087        See also:
4088        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes
4089        - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse
4090        - `OverviewUserInfo()` method
4091
4092        :param accountId: string with numeric account ID. If `None`, then used class field `accountId`.
4093        :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict.
4094                 Example of responses:
4095                 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`.
4096                 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000},
4097                                    "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000},
4098                                    "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000},
4099                                    "fundsSufficiencyLevel": {"units": "1", "nano": 280000000},
4100                                    "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`.
4101        """
4102        if accountId is None or not accountId:
4103            if self.accountId is None or not self.accountId:
4104                uLogger.error("Variable `accountId` must be defined for using this method!")
4105                raise Exception("Account ID required")
4106
4107            else:
4108                accountId = self.accountId  # use `self.accountId` (main ID) by default
4109
4110        uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId))
4111
4112        self.body = str({"accountId": accountId})
4113        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes"
4114        rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST")
4115
4116        if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}:
4117            uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId))
4118            rawMargin = {}
4119
4120        else:
4121            if self.moreDebug:
4122                uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId))
4123
4124        return rawMargin
4125
4126    def RequestTariffLimits(self) -> dict:
4127        """
4128        Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`.
4129
4130        See also:
4131        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff
4132        - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest
4133        - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit
4134        - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit
4135        - `OverviewUserInfo()` method
4136
4137        :return: dict with raw data from server that contains limits of current tariff. Example of dict:
4138                 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...],
4139                   "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`.
4140        """
4141        uLogger.debug("Requesting limits of current tariff. Wait, please...")
4142
4143        self.body = str({})
4144        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff"
4145        rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
4146
4147        if self.moreDebug:
4148            uLogger.debug("Records with limits of current tariff successfully received")
4149
4150        return rawTariffLimits
4151
4152    def RequestBondCoupons(self, iJSON: dict) -> dict:
4153        """
4154        Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown
4155        then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`.
4156        All dates are in UTC timezone.
4157
4158        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons
4159        Documentation:
4160        - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest
4161        - response: https://tinkoff.github.io/investAPI/instruments/#coupon
4162
4163        See also: `ExtendBondsData()`.
4164
4165        :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self._ticker]`
4166                      If raw iJSON is not data of bond then server returns an error [400] with message:
4167                      `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`.
4168        :return: dictionary with bond payment calendar. Response example
4169                 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12",
4170                   "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000},
4171                   "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z",
4172                   "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}`
4173        """
4174        if iJSON["figi"] is None or not iJSON["figi"]:
4175            uLogger.error("FIGI must be defined for using this method!")
4176            raise Exception("FIGI required")
4177
4178        startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z"
4179        endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z"
4180
4181        uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format(
4182            "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "",
4183            self._figi,
4184            startDate,
4185            endDate,
4186        ))
4187
4188        self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate})
4189        calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons"
4190        calendar = self.SendAPIRequest(calendarURL, reqType="POST")
4191
4192        if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}:
4193            uLogger.warning("Instrument type is not bond!")
4194
4195        else:
4196            if self.moreDebug:
4197                uLogger.debug("Records about bond payment calendar successfully received")
4198
4199        return calendar
4200
4201    def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame:
4202        """
4203        Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider
4204        Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar,
4205        coupon yields, current yields and some statistics etc.
4206
4207        WARNING! This is too long operation if a lot of bonds requested from broker server.
4208
4209        See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`.
4210
4211        :param instruments: list of strings with tickers or FIGIs.
4212        :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`,
4213                     for further used by data scientists or stock analytics.
4214        :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker.
4215                 In XLSX-file and Pandas DataFrame fields mean:
4216                 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond
4217                 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon
4218        """
4219        if instruments is None or not instruments:
4220            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
4221            raise Exception("Ticker or FIGI required")
4222
4223        if isinstance(instruments, str):
4224            instruments = [instruments]
4225
4226        uniqueInstruments = self.GetUniqueFIGIs(instruments)
4227
4228        uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...")
4229
4230        iCount = len(uniqueInstruments)
4231        tooLong = iCount >= 20
4232        if tooLong:
4233            uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...")
4234
4235        bonds = None
4236        for i, self._figi in enumerate(uniqueInstruments):
4237            instrument = self.SearchByFIGI(requestPrice=False)  # raw data about instrument from server
4238
4239            if "type" in instrument.keys() and instrument["type"] == "Bonds":
4240                # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond
4241                rawBond = self.SearchByFIGI(requestPrice=True)
4242
4243                # Widen raw data with UTC current time (iData["actualDateTime"]):
4244                actualDate = datetime.now(tzutc())
4245                iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond
4246
4247                # Widen raw data with bond payment calendar (iData["rawCalendar"]):
4248                iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)}
4249
4250                # Replace some values with human-readable:
4251                iData["nominalCurrency"] = iData["nominal"]["currency"]
4252                iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"])
4253                iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"])
4254                iData["aciCurrency"] = iData["aciValue"]["currency"]
4255                iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"])
4256                iData["issueSize"] = int(iData["issueSize"])
4257                iData["issueSizePlan"] = int(iData["issueSizePlan"])
4258                iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]]
4259                iData["step"] = iData["step"] if "step" in iData.keys() else 0
4260                iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]]
4261                iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0
4262                iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0
4263                iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0
4264                iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0
4265                iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0
4266                iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0
4267
4268                # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date):
4269                iData["limitUpPercent"] = iData["currentPrice"]["limitUp"]  # max price on current day in percents of nominal
4270                iData["limitDownPercent"] = iData["currentPrice"]["limitDown"]  # min price on current day in percents of nominal
4271                iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"]  # last price on market in percents of nominal
4272                iData["closePricePercent"] = iData["currentPrice"]["closePrice"]  # previous day close in percents of nominal
4273                iData["changes"] = iData["currentPrice"]["changes"]  # this is percent of changes between `currentPrice` and `lastPrice`
4274                iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100  # max price on current day is `limitUpPercent` * `nominal`
4275                iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100  # min price on current day is `limitDownPercent` * `nominal`
4276                iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100  # last price on market is `lastPricePercent` * `nominal`
4277                iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100  # previous day close is `closePricePercent` * `nominal`
4278                iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"]  # this is delta between last deal price and last close
4279
4280                # Widen raw data with calendar data from `rawCalendar` values:
4281                calendarData = []
4282                if "events" in iData["rawCalendar"].keys():
4283                    for item in iData["rawCalendar"]["events"]:
4284                        calendarData.append({
4285                            "couponDate": item["couponDate"],
4286                            "couponNumber": int(item["couponNumber"]),
4287                            "fixDate": item["fixDate"] if "fixDate" in item.keys() else "",
4288                            "payCurrency": item["payOneBond"]["currency"],
4289                            "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]),
4290                            "couponType": TKS_COUPON_TYPES[item["couponType"]],
4291                            "couponStartDate": item["couponStartDate"],
4292                            "couponEndDate": item["couponEndDate"],
4293                            "couponPeriod": item["couponPeriod"],
4294                        })
4295
4296                    # if maturity date is unknown then uses the latest date in bond payment calendar for it:
4297                    if "maturityDate" not in iData.keys():
4298                        iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else ""
4299
4300                # Widen raw data with Coupon Rate.
4301                # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%:
4302                iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData])
4303                iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData])
4304                iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0.
4305
4306                # Widen raw data with Yield to Maturity (YTM) on current date.
4307                # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%:
4308                maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None
4309                iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None
4310                iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate])
4311                iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"]  # sum of all last coupons minus current ACI value
4312                iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0.
4313
4314                iData["calendar"] = calendarData  # adds calendar at the end
4315
4316                # Remove not used data:
4317                iData.pop("uid")
4318                iData.pop("positionUid")
4319                iData.pop("currentPrice")
4320                iData.pop("rawCalendar")
4321
4322                colNames = list(iData.keys())
4323                if bonds is None:
4324                    bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames))
4325
4326                else:
4327                    bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True)
4328
4329            else:
4330                uLogger.warning("Instrument is not a bond!")
4331
4332            processed = round(100 * (i + 1) / iCount, 1)
4333            if tooLong and processed % 5 == 0:
4334                uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount))
4335
4336            else:
4337                uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount))
4338
4339        bonds.index = bonds["ticker"].tolist()  # replace indexes with ticker names
4340
4341        # Saving bonds from Pandas DataFrame to XLSX sheet:
4342        if xlsx and self.bondsXLSXFile:
4343            with pd.ExcelWriter(
4344                    path=self.bondsXLSXFile,
4345                    date_format=TKS_DATE_FORMAT,
4346                    datetime_format=TKS_DATE_TIME_FORMAT,
4347                    mode="w",
4348            ) as writer:
4349                bonds.to_excel(
4350                    writer,
4351                    sheet_name="Extended bonds data",
4352                    index=True,
4353                    encoding="UTF-8",
4354                    freeze_panes=(1, 1),
4355                )  # saving as XLSX-file with freeze first row and column as headers
4356
4357            uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile)))
4358
4359        return bonds
4360
4361    def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame:
4362        """
4363        Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default.
4364
4365        WARNING! This is too long operation if a lot of bonds requested from broker server.
4366
4367        See also: `ShowBondsCalendar()`, `ExtendBondsData()`.
4368
4369        :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
4370                        extended information about bonds: main info, current prices, bond payment calendar,
4371                        coupon yields, current yields and some statistics etc.
4372                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
4373        :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default,
4374                     for further used by data scientists or stock analytics.
4375        :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon
4376        """
4377        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4378            extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=False)
4379
4380        uLogger.debug("Generating bond payments calendar data. Wait, please...")
4381
4382        colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"]
4383        colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"]
4384        calendar = None
4385        for bond in extBonds.iterrows():
4386            for item in bond[1]["calendar"]:
4387                cData = {
4388                    "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()),
4389                    "couponDate": item["couponDate"],
4390                    "figi": bond[1]["figi"],
4391                    "ticker": bond[1]["ticker"],
4392                    "name": bond[1]["name"],
4393                    "couponNumber": item["couponNumber"],
4394                    "payOneBond": item["payOneBond"],
4395                    "payCurrency": item["payCurrency"],
4396                    "couponType": item["couponType"],
4397                    "couponPeriod": item["couponPeriod"],
4398                    "fixDate": item["fixDate"],
4399                    "couponStartDate": item["couponStartDate"],
4400                    "couponEndDate": item["couponEndDate"],
4401                }
4402
4403                if calendar is None:
4404                    calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID))
4405
4406                else:
4407                    calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True)
4408
4409        if calendar is not None:
4410            calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True)  # sort all payments for all bonds by payment date
4411
4412            # Saving calendar from Pandas DataFrame to XLSX sheet:
4413            if xlsx:
4414                xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx"
4415
4416                with pd.ExcelWriter(
4417                        path=xlsxCalendarFile,
4418                        date_format=TKS_DATE_FORMAT,
4419                        datetime_format=TKS_DATE_TIME_FORMAT,
4420                        mode="w",
4421                ) as writer:
4422                    humanReadable = calendar.copy(deep=True)
4423                    humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0])
4424                    humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0])
4425                    humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0])
4426                    humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0])
4427                    humanReadable.columns = colNames  # human-readable column names
4428
4429                    humanReadable.to_excel(
4430                        writer,
4431                        sheet_name="Bond payments calendar",
4432                        index=False,
4433                        encoding="UTF-8",
4434                        freeze_panes=(1, 2),
4435                    )  # saving as XLSX-file with freeze first row and column as headers
4436
4437                    del humanReadable  # release df in memory
4438
4439                uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile)))
4440
4441        return calendar
4442
4443    def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True, onlyFiles=False) -> str:
4444        """
4445        Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond.
4446        Also, creates Markdown file with calendar data, `calendar.md` by default.
4447
4448        See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`.
4449
4450        :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
4451                        extended information about bonds: main info, current prices, bond payment calendar,
4452                        coupon yields, current yields and some statistics etc.
4453                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
4454        :param show: if `True` then also printing bonds payment calendar to the console,
4455                     otherwise save to file `calendarFile` only. `False` by default.
4456        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
4457        :return: multilines text in Markdown format with bonds payment calendar as a table.
4458        """
4459        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4460            extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=show or onlyFiles)
4461
4462        infoText = "# Bond payments calendar\n\n"
4463
4464        calendar = self.CreateBondsCalendar(extBonds, xlsx=show or onlyFiles)  # generate Pandas DataFrame with full calendar data
4465
4466        if not (calendar is None or calendar.empty):
4467            splitLine = "|       |                 |              |              |     |               |           |        |                   |\n"
4468
4469            info = [
4470                "* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4471                "| Paid  | Payment date    | FIGI         | Ticker       | No. | Value         | Type      | Period | End registry date |\n",
4472                "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n",
4473            ]
4474
4475            newMonth = False
4476            notOneBond = calendar["figi"].nunique() > 1
4477            for i, bond in enumerate(calendar.iterrows()):
4478                if newMonth and notOneBond:
4479                    info.append(splitLine)
4480
4481                info.append(
4482                    "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format(
4483                        "  √" if bond[1]["paid"] else "  —",
4484                        bond[1]["couponDate"].split("T")[0],
4485                        bond[1]["figi"],
4486                        bond[1]["ticker"],
4487                        bond[1]["couponNumber"],
4488                        "{} {}".format(
4489                            "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."),
4490                            bond[1]["payCurrency"],
4491                        ),
4492                        bond[1]["couponType"],
4493                        bond[1]["couponPeriod"],
4494                        bond[1]["fixDate"].split("T")[0],
4495                    )
4496                )
4497
4498                if i < len(calendar.values) - 1:
4499                    curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4500                    nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4501                    newMonth = False if curDate.month == nextDate.month else True
4502
4503                else:
4504                    newMonth = False
4505
4506            infoText += "".join(info)
4507
4508            if show and not onlyFiles:
4509                uLogger.info("{}".format(infoText))
4510
4511            if self.calendarFile is not None and (show or onlyFiles):
4512                with open(self.calendarFile, "w", encoding="UTF-8") as fH:
4513                    fH.write(infoText)
4514
4515                uLogger.info("Bond payments calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile)))
4516
4517                if self.useHTMLReports:
4518                    htmlFilePath = self.calendarFile.replace(".md", ".html") if self.calendarFile.endswith(".md") else self.calendarFile + ".html"
4519                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
4520                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Bond payments calendar", commonCSS=COMMON_CSS, markdown=infoText))
4521
4522                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
4523
4524        else:
4525            infoText += "No data\n"
4526
4527        return infoText
4528
4529    def OverviewAccounts(self, show: bool = False, onlyFiles=False) -> dict:
4530        """
4531        Method for parsing and show simple table with all available user accounts.
4532
4533        See also: `RequestAccounts()` and `OverviewUserInfo()` methods.
4534
4535        :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log.
4536        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
4537        :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict:
4538                 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...},
4539                          "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1",
4540                                                        "status": "Opened and active account", "opened": "2018-05-23 00:00:00",
4541                                                        "closed": "—", "access": "Full access" }, ...}}`
4542        """
4543        rawAccounts = self.RequestAccounts()  # Raw responses with accounts
4544
4545        # This is an array of dict with user accounts, its `accountId`s and some parsed data:
4546        accounts = {
4547            item["id"]: {
4548                "type": TKS_ACCOUNT_TYPES[item["type"]],
4549                "name": item["name"],
4550                "status": TKS_ACCOUNT_STATUSES[item["status"]],
4551                "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4552                "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—",
4553                "access": TKS_ACCESS_LEVELS[item["accessLevel"]],
4554            } for item in rawAccounts["accounts"]
4555        }
4556
4557        # Raw and parsed data with some fields replaced in "stat" section:
4558        view = {
4559            "rawAccounts": rawAccounts,
4560            "stat": accounts,
4561        }
4562
4563        # --- Prepare simple text table with only accounts data in human-readable format:
4564        if show or onlyFiles:
4565            info = [
4566                "# User accounts\n\n",
4567                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4568                "| Account ID   | Type                      | Status                    | Name                           |\n",
4569                "|--------------|---------------------------|---------------------------|--------------------------------|\n",
4570            ]
4571
4572            for account in view["stat"].keys():
4573                info.extend([
4574                    "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format(
4575                        account,
4576                        view["stat"][account]["type"],
4577                        view["stat"][account]["status"],
4578                        view["stat"][account]["name"],
4579                    )
4580                ])
4581
4582            infoText = "".join(info)
4583
4584            if show and not onlyFiles:
4585                uLogger.info(infoText)
4586
4587            if self.userAccountsFile and (show or onlyFiles):
4588                with open(self.userAccountsFile, "w", encoding="UTF-8") as fH:
4589                    fH.write(infoText)
4590
4591                uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile)))
4592
4593                if self.useHTMLReports:
4594                    htmlFilePath = self.userAccountsFile.replace(".md", ".html") if self.userAccountsFile.endswith(".md") else self.userAccountsFile + ".html"
4595                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
4596                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User accounts", commonCSS=COMMON_CSS, markdown=infoText))
4597
4598                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
4599
4600        return view
4601
4602    def OverviewUserInfo(self, show: bool = False, onlyFiles=False) -> dict:
4603        """
4604        Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit).
4605
4606        See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods.
4607
4608        :param show: if `False` then only dictionary returns, if `True` then also print user's data to log.
4609        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
4610        :return: dict with raw parsed data from server and some calculated statistics about it.
4611        """
4612        overview = self.Overview(show=False)  # Request current user portfolio for the ability to calculate missing funds
4613        tmpTicker = self._ticker
4614        self._ticker = "RUB000UTSTOM"  # This instrument show in rub how much money cost current margin
4615        missing = self.GetInstrumentFromPortfolio(portfolio=overview)
4616        self._ticker = tmpTicker
4617
4618        rawUserInfo = self.RequestUserInfo()  # Raw response with common user info
4619        overviewAccount = self.OverviewAccounts(show=False)  # Raw and parsed accounts data
4620        rawAccounts = overviewAccount["rawAccounts"]  # Raw response with user accounts data
4621        accounts = overviewAccount["stat"]  # Dict with only statistics about user accounts
4622        rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()}  # Raw response with margin calculation for every account ID
4623        rawTariffLimits = self.RequestTariffLimits()  # Raw response with limits of current tariff
4624
4625        # This is dict with parsed common user data:
4626        userInfo = {
4627            "premium": "Yes" if rawUserInfo["premStatus"] else "No",
4628            "qualified": "Yes" if rawUserInfo["qualStatus"] else "No",
4629            "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]],
4630            "tariff": rawUserInfo["tariff"],
4631        }
4632
4633        # This is an array of dict with parsed margin statuses for every account IDs:
4634        margins = {}
4635        for accountId in accounts.keys():
4636            if rawMargins[accountId]:
4637                margins[accountId] = {
4638                    "currency": rawMargins[accountId]["liquidPortfolio"]["currency"],
4639                    "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]),
4640                    "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]),
4641                    "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]),
4642                    "diff": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]),
4643                    "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]),
4644                    "missing": missing["volume"],
4645                }
4646
4647            else:
4648                margins[accountId] = {}  # Server response: margin status is disabled for current accountId
4649
4650        unary = {}  # unary-connection limits
4651        for item in rawTariffLimits["unaryLimits"]:
4652            if item["limitPerMinute"] in unary.keys():
4653                unary[item["limitPerMinute"]].extend(item["methods"])
4654
4655            else:
4656                unary[item["limitPerMinute"]] = item["methods"]
4657
4658        stream = {}  # stream-connection limits
4659        for item in rawTariffLimits["streamLimits"]:
4660            if item["limit"] in stream.keys():
4661                stream[item["limit"]].extend(item["streams"])
4662
4663            else:
4664                stream[item["limit"]] = item["streams"]
4665
4666        # This is dict with parsed limits of current tariff (connections, API methods etc.):
4667        limits = {
4668            "unary": unary,
4669            "stream": stream,
4670        }
4671
4672        # Raw and parsed data as an output result:
4673        view = {
4674            "rawUserInfo": rawUserInfo,
4675            "rawAccounts": rawAccounts,
4676            "rawMargins": rawMargins,
4677            "rawTariffLimits": rawTariffLimits,
4678            "stat": {
4679                "overview": overview,
4680                "userInfo": userInfo,
4681                "accounts": accounts,
4682                "margins": margins,
4683                "limits": limits,
4684            },
4685        }
4686
4687        # --- Prepare text table with user information in human-readable format:
4688        if show or onlyFiles:
4689            info = [
4690                "# Full user information\n\n",
4691                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4692                "## Common information\n\n",
4693                "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]),
4694                "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]),
4695                "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]),
4696                "* **Allowed to work with instruments:**\n{}\n".format("".join(["  - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])),
4697                "\n## User accounts\n\n",
4698            ]
4699
4700            for account in view["stat"]["accounts"].keys():
4701                info.extend([
4702                    "### ID: [{}]\n\n".format(account),
4703                    "| Parameters           | Values                                                       |\n",
4704                    "|----------------------|--------------------------------------------------------------|\n",
4705                    "| Account type:        | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]),
4706                    "| Account name:        | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]),
4707                    "| Account status:      | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]),
4708                    "| Access level:        | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]),
4709                    "| Date opened:         | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]),
4710                    "| Date closed:         | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]),
4711                ])
4712
4713                if margins[account]:
4714                    info.extend([
4715                        "| Margin status:       | Enabled                                                      |\n",
4716                        "| - Liquid portfolio:  | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])),
4717                        "| - Margin starting:   | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])),
4718                        "| - Margin minimum:    | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])),
4719                        "| - Margin difference: | {:<60} |\n".format("{} {}".format(margins[account]["diff"], margins[account]["currency"])),
4720                        "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)),
4721                        "| - Not covered funds: | {:<60} |\n\n".format("{:.2f} {}".format(margins[account]["missing"], margins[account]["currency"])),
4722                    ])
4723
4724                else:
4725                    info.append("| Margin status:       | Disabled                                                     |\n\n")
4726
4727            info.extend([
4728                "\n## Current user tariff limits\n",
4729                "\n### See also\n",
4730                "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n",
4731                "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n",
4732                "  - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n",
4733                "  - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n",
4734                "\n### Unary limits\n",
4735            ])
4736
4737            if unary:
4738                for key, values in sorted(unary.items()):
4739                    info.append("\n* Max requests per minute: {}\n".format(key))
4740
4741                    for value in values:
4742                        info.append("  - {}\n".format(value))
4743
4744            else:
4745                info.append("\nNot available\n")
4746
4747            info.append("\n### Stream limits\n")
4748
4749            if stream:
4750                for key, values in sorted(stream.items()):
4751                    info.append("\n* Max stream connections: {}\n".format(key))
4752
4753                    for value in values:
4754                        info.append("  - {}\n".format(value))
4755
4756            else:
4757                info.append("\nNot available\n")
4758
4759            infoText = "".join(info)
4760
4761            if show and not onlyFiles:
4762                uLogger.info(infoText)
4763
4764            if self.userInfoFile and (show or onlyFiles):
4765                with open(self.userInfoFile, "w", encoding="UTF-8") as fH:
4766                    fH.write(infoText)
4767
4768                uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile)))
4769
4770                if self.useHTMLReports:
4771                    htmlFilePath = self.userInfoFile.replace(".md", ".html") if self.userInfoFile.endswith(".md") else self.userInfoFile + ".html"
4772                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
4773                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User info", commonCSS=COMMON_CSS, markdown=infoText))
4774
4775                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
4776
4777        return view

This class implements methods to work with Tinkoff broker server.

Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/

About token: https://tinkoff.github.io/investAPI/token/

TinkoffBrokerServer( token: str, accountId: str = None, useCache: bool = True, defaultCache: str = 'dump.json')
 86    def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None:
 87        """
 88        Main class init.
 89
 90        :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`.
 91        :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports.
 92                          Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.
 93        :param useCache: use default cache file with raw data to use instead of `iList`.
 94                         True by default. Cache is auto-update if new day has come.
 95                         If you don't want to use cache and always updates raw data then set `useCache=False`.
 96        :param defaultCache: path to default cache file. `dump.json` by default.
 97        """
 98        if token is None or not token:
 99            try:
100                self.token = r"{}".format(os.environ["TKS_API_TOKEN"])
101                uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/")
102
103            except KeyError:
104                uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/")
105                raise Exception("Token required")
106
107        else:
108            self.token = token  # highly priority than environment variable 'TKS_API_TOKEN'
109            uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`")
110
111        if accountId is None or not accountId:
112            try:
113                self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"])
114                uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId))
115
116            except KeyError:
117                uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).")
118
119        else:
120            self.accountId = accountId  # highly priority than environment variable 'TKS_ACCOUNT_ID'
121            uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId))
122
123        self.version = __version__  # duplicate here used TKSBrokerAPI main version
124        """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only.
125
126        Latest version: https://pypi.org/project/tksbrokerapi/
127        """
128
129        self._tag = ""
130        """Identification TKSBrokerAPI tag in log messages to simplify debugging when platform instances runs in parallel mode. Default: `""` (empty string)."""
131
132        self.__lock = Lock()  # initialize multiprocessing mutex lock
133
134        self.aliases = TKS_TICKER_ALIASES
135        """Some aliases instead official tickers.
136
137        See also: `TKSEnums.TKS_TICKER_ALIASES`
138        """
139
140        self.aliasesKeys = self.aliases.keys()  # re-calc only first time at class init
141
142        self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED  # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there
143
144        self._ticker = ""
145        """String with ticker, e.g. `GOOGL`. Tickers may be upper case only.
146
147        Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc.
148        More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`.
149
150        See also: `SearchByTicker()`, `SearchInstruments()`.
151        """
152
153        self._figi = ""
154        """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only.
155
156        See also: `SearchByFIGI()`, `SearchInstruments()`.
157        """
158
159        self.depth = 1
160        """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI.
161
162        See also: `GetCurrentPrices()`.
163        """
164
165        self.server = r"https://invest-public-api.tinkoff.ru/rest"
166        """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest
167
168        See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`.
169        """
170
171        uLogger.debug("Broker API server: {}".format(self.server))
172
173        self.timeout = 15
174        """Server operations timeout in seconds. Default: `15`.
175
176        See also: `SendAPIRequest()`.
177        """
178
179        self.headers = {
180            "Content-Type": "application/json",
181            "accept": "application/json",
182            "Authorization": "Bearer {}".format(self.token),
183            "x-app-name": "Tim55667757.TKSBrokerAPI",
184        }
185        """
186        Headers which send in every request to broker server. Please, do not change it!
187        Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}", "x-app-name": "Tim55667757.TKSBrokerAPI"}`.
188
189        See also: `SendAPIRequest()`.
190        """
191
192        self.body = None
193        """Request body which send to broker server. Default: `None`.
194
195        See also: `SendAPIRequest()`.
196        """
197
198        self.moreDebug = False
199        """Enables more debug information in this class, such as net request and response headers in all methods. `False` by default."""
200
201        self.useHTMLReports = False
202        """
203        If `True` then TKSBrokerAPI generate also HTML reports from Markdown. `False` by default.
204        
205        See also: Mako Templates for Python (https://www.makotemplates.org/). Mako is a template library provides simple syntax and maximum performance.
206        """
207
208        self.historyFile = None
209        """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame.
210
211        See also: `History()`.
212        """
213
214        self.htmlHistoryFile = "index.html"
215        """Full path to the html file where rendered candles chart stored. Default: `index.html`.
216
217        See also: `ShowHistoryChart()`.
218        """
219
220        self.instrumentsFile = "instruments.md"
221        """Filename where full available to user instruments list will be saved. Default: `instruments.md`.
222
223        See also: `ShowInstrumentsInfo()`.
224        """
225
226        self.searchResultsFile = "search-results.md"
227        """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`.
228
229        See also: `SearchInstruments()`.
230        """
231
232        self.pricesFile = "prices.md"
233        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
234
235        See also: `GetListOfPrices()`.
236        """
237
238        self.infoFile = "info.md"
239        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
240
241        See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`.
242        """
243
244        self.bondsXLSXFile = "ext-bonds.xlsx"
245        """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 
246        bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`.
247
248        See also: `ExtendBondsData()`.
249        """
250
251        self.calendarFile = "calendar.md"
252        """Filename where bonds payment calendar will be saved. Default: `calendar.md`.
253        
254        Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`.
255
256        See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`.
257        """
258
259        self.overviewFile = "overview.md"
260        """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`.
261
262        See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`.
263        """
264
265        self.overviewDigestFile = "overview-digest.md"
266        """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`.
267
268        See also: `Overview()` with parameter `details="digest"`.
269        """
270
271        self.overviewPositionsFile = "overview-positions.md"
272        """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`.
273
274        See also: `Overview()` with parameter `details="positions"`.
275        """
276
277        self.overviewOrdersFile = "overview-orders.md"
278        """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`.
279
280        See also: `Overview()` with parameter `details="orders"`.
281        """
282
283        self.overviewAnalyticsFile = "overview-analytics.md"
284        """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`.
285
286        See also: `Overview()` with parameter `details="analytics"`.
287        """
288
289        self.overviewBondsCalendarFile = "overview-calendar.md"
290        """Filename where only the bonds calendar section will be saved. Default: `overview-calendar.md`.
291
292        See also: `Overview()` with parameter `details="calendar"`.
293        """
294
295        self.reportFile = "deals.md"
296        """Filename where history of deals and trade statistics will be saved. Default: `deals.md`.
297
298        See also: `Deals()`.
299        """
300
301        self.withdrawalLimitsFile = "limits.md"
302        """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`.
303
304        See also: `OverviewLimits()` and `RequestLimits()`.
305        """
306
307        self.userInfoFile = "user-info.md"
308        """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`.
309
310        See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`.
311        """
312
313        self.userAccountsFile = "accounts.md"
314        """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`.
315
316        See also: `OverviewAccounts()`, `RequestAccounts()`.
317        """
318
319        self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache
320        """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`.
321
322        Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`.
323
324        See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`.
325        """
326
327        self.iList = None  # init iList for raw instruments data
328        """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`.
329        
330        See also: `Listing()`, `DumpInstruments()`.
331        """
332
333        # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server:
334        if useCache:
335            if os.path.exists(self.iListDumpFile):
336                dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc())  # dump modification date and time
337                curTime = datetime.now(tzutc())
338
339                if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year):
340                    uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
341
342                    self.DumpInstruments(forceUpdate=True)  # updating self.iList and dump file
343
344                else:
345                    self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8"))  # load iList from dump
346
347                    uLogger.debug("Local cache with raw instruments data is used: [{}]. Last modified: [{}] UTC".format(
348                        os.path.abspath(self.iListDumpFile),
349                        dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT),
350                    ))
351
352            else:
353                uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...")
354                self.DumpInstruments(forceUpdate=True)  # updating self.iList and creating default dump file
355
356        else:
357            self.iList = self.Listing()  # request new raw instruments data from broker server
358            self.DumpInstruments(forceUpdate=False)  # save raw instrument's data to default dump file `iListDumpFile`
359
360        self.priceModel = PriceGenerator()  # init PriceGenerator object to work with candles data
361        """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on.
362
363        See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator
364        """

Main class init.

Parameters
  • token: Bearer token for Tinkoff Invest API. It can be set from environment variable TKS_API_TOKEN.
  • accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports. Also, this variable can be set from environment variable TKS_ACCOUNT_ID.
  • useCache: use default cache file with raw data to use instead of iList. True by default. Cache is auto-update if new day has come. If you don't want to use cache and always updates raw data then set useCache=False.
  • defaultCache: path to default cache file. dump.json by default.
version

Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only.

Latest version: https://pypi.org/project/tksbrokerapi/

aliases

Some aliases instead official tickers.

See also: TKSEnums.TKS_TICKER_ALIASES

depth

Depth of Market (DOM) can be >= 1. Default: 1. It used with --price key to showing DOM with current prices for givens ticker or FIGI.

See also: GetCurrentPrices().

server

Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest

See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and SendAPIRequest().

timeout

Server operations timeout in seconds. Default: 15.

See also: SendAPIRequest().

headers

Headers which send in every request to broker server. Please, do not change it! Default: {"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}", "x-app-name": "Tim55667757.TKSBrokerAPI"}.

See also: SendAPIRequest().

body

Request body which send to broker server. Default: None.

See also: SendAPIRequest().

moreDebug

Enables more debug information in this class, such as net request and response headers in all methods. False by default.

useHTMLReports

If True then TKSBrokerAPI generate also HTML reports from Markdown. False by default.

See also: Mako Templates for Python (https://www.makotemplates.org/). Mako is a template library provides simple syntax and maximum performance.

historyFile

Full path to the output file where history candles will be saved or updated. Default: None, it mean that returns only Pandas DataFrame.

See also: History().

htmlHistoryFile

Full path to the html file where rendered candles chart stored. Default: index.html.

See also: ShowHistoryChart().

instrumentsFile

Filename where full available to user instruments list will be saved. Default: instruments.md.

See also: ShowInstrumentsInfo().

searchResultsFile

Filename with all found instruments searched by part of its ticker, FIGI or name. Default: search-results.md.

See also: SearchInstruments().

pricesFile

Filename where prices of selected instruments will be saved. Default: prices.md.

See also: GetListOfPrices().

infoFile

Filename where prices of selected instruments will be saved. Default: prices.md.

See also: ShowInstrumentsInfo(), RequestBondCoupons() and RequestTradingStatus().

bondsXLSXFile

Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, bonds payment calendar, some statistics will be stored. Default: ext-bonds.xlsx.

See also: ExtendBondsData().

calendarFile

Filename where bonds payment calendar will be saved. Default: calendar.md.

Pandas dataframe with only bonds payment calendar also will be stored to default file calendar.xlsx.

See also: CreateBondsCalendar(), ShowBondsCalendar(), ShowInstrumentInfo(), RequestBondCoupons() and ExtendBondsData().

overviewFile

Filename where current portfolio, open trades and orders will be saved. Default: overview.md.

See also: Overview(), RequestPortfolio(), RequestPositions(), RequestPendingOrders() and RequestStopOrders().

overviewDigestFile

Filename where short digest of the portfolio status will be saved. Default: overview-digest.md.

See also: Overview() with parameter details="digest".

overviewPositionsFile

Filename where only open positions, without everything else will be saved. Default: overview-positions.md.

See also: Overview() with parameter details="positions".

overviewOrdersFile

Filename where open limits and stop orders will be saved. Default: overview-orders.md.

See also: Overview() with parameter details="orders".

overviewAnalyticsFile

Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: overview-analytics.md.

See also: Overview() with parameter details="analytics".

overviewBondsCalendarFile

Filename where only the bonds calendar section will be saved. Default: overview-calendar.md.

See also: Overview() with parameter details="calendar".

reportFile

Filename where history of deals and trade statistics will be saved. Default: deals.md.

See also: Deals().

withdrawalLimitsFile

Filename where table of funds available for withdrawal will be saved. Default: limits.md.

See also: OverviewLimits() and RequestLimits().

userInfoFile

Filename where all available user's data (accountIds, common user information, margin status and tariff connections limit) will be saved. Default: user-info.md.

See also: OverviewUserInfo(), RequestAccounts(), RequestUserInfo(), RequestMarginStatus() and RequestTariffLimits().

userAccountsFile

Filename where simple table with all available user accounts (accountIds) will be saved. Default: accounts.md.

See also: OverviewAccounts(), RequestAccounts().

iListDumpFile

Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: dump.json.

Pandas dataframe with raw instruments data also will be stored to default file dump.xlsx.

See also: DumpInstruments() and DumpInstrumentsAsXLSX().

iList

Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the iListDumpFile.

See also: Listing(), DumpInstruments().

priceModel

PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on.

See also: LoadHistory(), ShowHistoryChart() and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator

tag: str

Setter for Identification TKSBrokerAPI tag in log messages to simplify debugging when platform instances runs in parallel mode. Default: "" (empty string).

ticker: str

Setter for string with ticker, e.g. GOOGL. Tickers may be upper case only.

Use alias for USD000UTSTOM simple as USD, EUR_RUB__TOM as EUR etc. More tickers aliases here: TKSEnums.TKS_TICKER_ALIASES.

See also: SearchByTicker(), SearchInstruments().

figi: str

Setter for string with FIGI, e.g. ticker GOOGL has FIGI BBG009S39JX6. FIGIs may be upper case only.

See also: SearchByFIGI(), SearchInstruments().

def SendAPIRequest( self, url: str, reqType: str = 'GET', retry: int = 3, pause: int = 5) -> dict:
446    def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5) -> dict:
447        """
448        Send GET or POST request to broker server and receive JSON object.
449
450        self.header: must be defining with dictionary of headers.
451        self.body: if define then used as request body. None by default.
452        self.timeout: global request timeout, 15 seconds by default.
453        :param url: url with REST request.
454        :param reqType: send "GET" or "POST" request. "GET" by default.
455        :param retry: how many times retry after first request if an 5xx server errors occurred.
456        :param pause: sleep time in seconds between retries.
457        :return: response JSON (dictionary) from broker.
458        """
459        if reqType.upper() not in ("GET", "POST"):
460            uLogger.error("You can define request type: `GET` or `POST`!")
461            raise Exception("Incorrect value")
462
463        if self.moreDebug:
464            uLogger.debug("Request parameters:")
465            uLogger.debug("    - REST API URL: {}".format(url))
466            uLogger.debug("    - request type: {}".format(reqType))
467            uLogger.debug("    - headers:\n{}".format(str(self.headers).replace(self.token, "*** request token ***")))
468            uLogger.debug("    - body:\n{}".format(self.body))
469
470        # fast hack to avoid all operations with some tickers/FIGI
471        responseJSON = {}
472        oK = True
473        for item in self.exclude:
474            if item in url:
475                if self.moreDebug:
476                    uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude)))
477
478                oK = False
479                break
480
481        if oK:
482            with self.__lock:  # acquire the mutex lock
483                counter = 0
484                response = None
485                errMsg = ""
486
487                while not response and counter <= retry:
488                    if reqType == "GET":
489                        response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout)
490
491                    if reqType == "POST":
492                        response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout)
493
494                    if self.moreDebug:
495                        uLogger.debug("Response:")
496                        uLogger.debug("    - status code: {}".format(response.status_code))
497                        uLogger.debug("    - reason: {}".format(response.reason))
498                        uLogger.debug("    - body length: {}".format(len(response.text)))
499                        uLogger.debug("    - headers:\n{}".format(response.headers))
500
501                    # Server returns some headers:
502                    # - `x-ratelimit-limit` — shows the settings of the current user limit for this method.
503                    # - `x-ratelimit-remaining` — the number of remaining requests of this type per minute.
504                    # - `x-ratelimit-reset` — time in seconds before resetting the request counter.
505                    # See: https://tinkoff.github.io/investAPI/grpc/#kreya
506                    if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0":
507                        rateLimitWait = int(response.headers["x-ratelimit-reset"])
508                        uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait))
509                        sleep(rateLimitWait)
510
511                    # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes
512                    if 400 <= response.status_code < 500:
513                        msg = "status code: [{}], response body: {}".format(response.status_code, response.text)
514                        uLogger.debug("    - not oK, but do not retry for 4xx errors, {}".format(msg))
515
516                        if "code" in response.text and "message" in response.text:
517                            msgDict = self._ParseJSON(rawData=response.text)
518                            uLogger.warning("HTTP-status code [{}], server message: {}".format(response.status_code, msgDict["message"]))
519
520                        counter = retry + 1  # do not retry for 4xx errors
521
522                    if 500 <= response.status_code < 600:
523                        errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text)
524                        uLogger.debug("    - not oK, {}".format(errMsg))
525
526                        if "code" in response.text and "message" in response.text:
527                            errMsgDict = self._ParseJSON(rawData=response.text)
528                            uLogger.warning("HTTP-status code [{}], error message: {}".format(response.status_code, errMsgDict["message"]))
529
530                        counter += 1
531
532                        if counter <= retry:
533                            uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause))
534                            sleep(pause)
535
536                responseJSON = self._ParseJSON(rawData=response.text)
537
538                if errMsg:
539                    uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/")
540                    uLogger.error("    - not oK, {}".format(errMsg))
541
542        return responseJSON

Send GET or POST request to broker server and receive JSON object.

self.header: must be defining with dictionary of headers. self.body: if define then used as request body. None by default. self.timeout: global request timeout, 15 seconds by default.

Parameters
  • url: url with REST request.
  • reqType: send "GET" or "POST" request. "GET" by default.
  • retry: how many times retry after first request if an 5xx server errors occurred.
  • pause: sleep time in seconds between retries.
Returns

response JSON (dictionary) from broker.

def Listing(self) -> dict:
575    def Listing(self) -> dict:
576        """
577        Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server.
578
579        :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures.
580        """
581        uLogger.debug("Requesting all available instruments for current account. Wait, please...")
582        uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES))
583
584        # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService
585        # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list.
586        iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS]
587
588        poolUpdater = ThreadPool(processes=CPU_USAGES)  # create pool for update instruments in parallel mode
589        listing = poolUpdater.map(self._IWrapper, iParams)  # execute update operations
590        poolUpdater.close()  # close the thread pool
591        poolUpdater.join()  # wait a moment until all data returns from threads
592
593        # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures.
594        # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method
595        iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing}
596
597        # calculate minimum price increment (step) for all instruments and set up instrument's type:
598        for iType in iList.keys():
599            for ticker in iList[iType]:
600                iList[iType][ticker]["type"] = iType
601
602                if "minPriceIncrement" in iList[iType][ticker].keys():
603                    iList[iType][ticker]["step"] = NanoToFloat(
604                        iList[iType][ticker]["minPriceIncrement"]["units"],
605                        iList[iType][ticker]["minPriceIncrement"]["nano"],
606                    )
607
608                else:
609                    iList[iType][ticker]["step"] = 0  # hack to avoid empty value in some instruments, e.g. futures
610
611        return iList

Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server.

Returns

Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures.

def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None:
613    def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None:
614        """
615        Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics.
616
617        See also: `DumpInstruments()`, `Listing()`.
618
619        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
620                            otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) .
621        """
622        if self.iListDumpFile is None or not self.iListDumpFile:
623            uLogger.error("Output name of dump file must be defined!")
624            raise Exception("Filename required")
625
626        if not self.iList or forceUpdate:
627            self.iList = self.Listing()
628
629        xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx"
630
631        # Save as XLSX with separated sheets for every type of instruments:
632        with pd.ExcelWriter(
633                path=xlsxDumpFile,
634                date_format=TKS_DATE_FORMAT,
635                datetime_format=TKS_DATE_TIME_FORMAT,
636                mode="w",
637        ) as writer:
638            for iType in TKS_INSTRUMENTS:
639                df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index")  # generate pandas object from self.iList dictionary
640                df = df[sorted(df)]  # sorted by column names
641                df = df.applymap(
642                    lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item,
643                    na_action="ignore",
644                )  # converting numbers from nano-type to float in every cell
645                df.to_excel(
646                    writer,
647                    sheet_name=iType,
648                    encoding="UTF-8",
649                    freeze_panes=(1, 1),
650                )  # saving as XLSX-file with freeze first row and column as headers
651
652        uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile)))

Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics.

See also: DumpInstruments(), Listing().

Parameters
  • forceUpdate: if True then at first updates data with Listing() method, otherwise just saves exist iList as XLSX-file (default: dump.xlsx) .
def DumpInstruments(self, forceUpdate: bool = True) -> str:
654    def DumpInstruments(self, forceUpdate: bool = True) -> str:
655        """
656        Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server
657        using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file.
658
659        See also: `DumpInstrumentsAsXLSX()`, `Listing()`.
660
661        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
662                            otherwise just saves exist `iList` as JSON-file (default: `dump.json`).
663        :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file.
664        """
665        if self.iListDumpFile is None or not self.iListDumpFile:
666            uLogger.error("Output name of dump file must be defined!")
667            raise Exception("Filename required")
668
669        if not self.iList or forceUpdate:
670            self.iList = self.Listing()
671
672        jsonDump = json.dumps(self.iList, indent=4, sort_keys=False)  # create JSON object as string
673        with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH:
674            fH.write(jsonDump)
675
676        uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile)))
677
678        return jsonDump

Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server using Listing() method. If iListDumpFile string is not empty then also save information to this file.

See also: DumpInstrumentsAsXLSX(), Listing().

Parameters
  • forceUpdate: if True then at first updates data with Listing() method, otherwise just saves exist iList as JSON-file (default: dump.json).
Returns

serialized JSON formatted str with full data of instruments, also saved to the --output JSON-file.

def ShowInstrumentInfo(self, iJSON: dict, show: bool = True, onlyFiles=False) -> str:
680    def ShowInstrumentInfo(self, iJSON: dict, show: bool = True, onlyFiles=False) -> str:
681        """
682        Show information about one instrument defined by json data and prints it in Markdown format.
683
684        See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`.
685
686        :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self._ticker]`
687        :param show: if `True` then also printing information about instrument and its current price.
688        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
689        :return: multilines text in Markdown format with information about one instrument.
690        """
691        splitLine = "|                                                             |                                                        |\n"
692        infoText = ""
693
694        if iJSON is not None and iJSON and isinstance(iJSON, dict):
695            info = [
696                "# Main information\n\n",
697                "* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
698                "| Parameters                                                  | Values                                                 |\n",
699                "|-------------------------------------------------------------|--------------------------------------------------------|\n",
700                "| Ticker:                                                     | {:<54} |\n".format(iJSON["ticker"]),
701                "| Full name:                                                  | {:<54} |\n".format(iJSON["name"]),
702            ]
703
704            if "sector" in iJSON.keys() and iJSON["sector"]:
705                info.append("| Sector:                                                     | {:<54} |\n".format(iJSON["sector"]))
706
707            if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] and "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"]:
708                info.append("| Country of instrument:                                      | {:<54} |\n".format("({}) {}".format(iJSON["countryOfRisk"], iJSON["countryOfRiskName"])))
709
710            info.extend([
711                splitLine,
712                "| FIGI (Financial Instrument Global Identifier):              | {:<54} |\n".format(iJSON["figi"]),
713                "| Real exchange [Exchange section]:                           | {:<54} |\n".format("{} [{}]".format(TKS_REAL_EXCHANGES[iJSON["realExchange"]], iJSON["exchange"])),
714            ])
715
716            if "isin" in iJSON.keys() and iJSON["isin"]:
717                info.append("| ISIN (International Securities Identification Number):      | {:<54} |\n".format(iJSON["isin"]))
718
719            if "classCode" in iJSON.keys():
720                info.append("| Class Code (exchange section where instrument is traded):   | {:<54} |\n".format(iJSON["classCode"]))
721
722            info.extend([
723                splitLine,
724                "| Current broker security trading status:                     | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]),
725                splitLine,
726                "| Buy operations allowed:                                     | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"),
727                "| Sale operations allowed:                                    | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"),
728                "| Short positions allowed:                                    | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"),
729            ])
730
731            if iJSON["figi"]:
732                self._figi = iJSON["figi"]
733                iJSON = iJSON | self.RequestTradingStatus()
734
735                info.extend([
736                    splitLine,
737                    "| Limit orders allowed:                                       | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"),
738                    "| Market orders allowed:                                      | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"),
739                    "| API trade allowed:                                          | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"),
740                ])
741
742            info.append(splitLine)
743
744            if "type" in iJSON.keys() and iJSON["type"]:
745                info.append("| Type of the instrument:                                     | {:<54} |\n".format(iJSON["type"]))
746
747                if "shareType" in iJSON.keys() and iJSON["shareType"]:
748                    info.append("| Share type:                                                 | {:<54} |\n".format(TKS_SHARE_TYPES[iJSON["shareType"]]))
749
750            if "futuresType" in iJSON.keys() and iJSON["futuresType"]:
751                info.append("| Futures type:                                               | {:<54} |\n".format(iJSON["futuresType"]))
752
753            if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]:
754                info.append("| IPO date:                                                   | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", "")))
755
756            if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]:
757                info.append("| Released date:                                              | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", "")))
758
759            if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]:
760                info.append("| Rebalancing frequency:                                      | {:<54} |\n".format(iJSON["rebalancingFreq"]))
761
762            if "focusType" in iJSON.keys() and iJSON["focusType"]:
763                info.append("| Focusing type:                                              | {:<54} |\n".format(iJSON["focusType"]))
764
765            if "assetType" in iJSON.keys() and iJSON["assetType"]:
766                info.append("| Asset type:                                                 | {:<54} |\n".format(iJSON["assetType"]))
767
768            if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]:
769                info.append("| Basic asset:                                                | {:<54} |\n".format(iJSON["basicAsset"]))
770
771            if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]:
772                info.append("| Basic asset size:                                           | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"]))))
773
774            if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]:
775                info.append("| ISO currency name:                                          | {:<54} |\n".format(iJSON["isoCurrencyName"]))
776
777            if "currency" in iJSON.keys():
778                info.append("| Payment currency:                                           | {:<54} |\n".format(iJSON["currency"]))
779
780            if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys():
781                info.append("| Nominal currency:                                           | {:<54} |\n".format(iJSON["nominal"]["currency"]))
782
783            if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]:
784                info.append("| First trade date:                                           | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", "")))
785
786            if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]:
787                info.append("| Last trade date:                                            | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", "")))
788
789            if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]:
790                info.append("| Date of expiration:                                         | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", "")))
791
792            if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]:
793                info.append("| State registration date:                                    | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", "")))
794
795            if "placementDate" in iJSON.keys() and iJSON["placementDate"]:
796                info.append("| Placement date:                                             | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", "")))
797
798            if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]:
799                info.append("| Maturity date:                                              | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", "")))
800
801            if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]:
802                info.append("| Perpetual bond:                                             | Yes                                                    |\n")
803
804            if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]:
805                info.append("| Over-the-counter (OTC) securities:                          | Yes                                                    |\n")
806
807            iExt = None
808            if iJSON["type"] == "Bonds":
809                info.extend([
810                    splitLine,
811                    "| Bond issue (size / plan):                                   | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])),
812                    "| Nominal price (100%):                                       | {:<54} |\n".format("{} {}".format(
813                        "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."),
814                        iJSON["nominal"]["currency"],
815                    )),
816                ])
817
818                if "floatingCouponFlag" in iJSON.keys():
819                    info.append("| Floating coupon:                                            | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No"))
820
821                if "amortizationFlag" in iJSON.keys():
822                    info.append("| Amortization:                                               | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No"))
823
824                info.append(splitLine)
825
826                if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]:
827                    info.append("| Number of coupon payments per year:                         | {:<54} |\n".format(iJSON["couponQuantityPerYear"]))
828
829                if iJSON["figi"]:
830                    iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False)  # extended bonds data
831
832                    info.extend([
833                        "| Days last to maturity date:                                 | {:<54} |\n".format(iExt["daysToMaturity"][0]),
834                        "| Coupons yield (average coupon daily yield * 365):           | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])),
835                        "| Current price yield (average daily yield * 365):            | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])),
836                    ])
837
838                if "aciValue" in iJSON.keys() and iJSON["aciValue"]:
839                    info.append("| Current accumulated coupon income (ACI):                    | {:<54} |\n".format("{:.2f} {}".format(
840                        NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]),
841                        iJSON["aciValue"]["currency"]
842                    )))
843
844            if "currentPrice" in iJSON.keys():
845                info.append(splitLine)
846
847                currency = iJSON["currency"] if "currency" in iJSON.keys() else ""  # nominal currency for bonds, otherwise currency of instrument
848                aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else ""  # payment currency
849
850                bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0  # previous close price of bond
851                bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0  # last price of bond
852                bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0  # max price of bond
853                bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0  # min price of bond
854                bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0  # delta between last deal price and last close
855
856                curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0
857                curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0
858
859                info.extend([
860                    "| Previous close price of the instrument:                     | {:<54} |\n".format("{}{}".format(
861                        "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A",
862                        "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
863                    )),
864                    "| Last deal price of the instrument:                          | {:<54} |\n".format("{}{}".format(
865                        "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A",
866                        "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
867                    )),
868                    "| Changes between last deal price and last close              | {:<54} |\n".format(
869                        "{:.2f}%{}".format(
870                            iJSON["currentPrice"]["changes"],
871                            " ({}{:.2f} {})".format(
872                                "+" if bondChangesDelta > 0 else "",
873                                bondChangesDelta,
874                                aciCurrency
875                            ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format(
876                                "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "",
877                                iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"],
878                                currency
879                            ),
880                        )
881                    ),
882                    "| Current limit price, min / max:                             | {:<54} |\n".format("{}{} / {}{}{}".format(
883                        "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A",
884                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
885                        "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A",
886                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
887                        " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else ""
888                    )),
889                    "| Actual price, sell / buy:                                   | {:<54} |\n".format("{}{} / {}{}{}".format(
890                        "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A",
891                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
892                        "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A",
893                        "%" if iJSON["type"] == "Bonds" else" {}".format(currency),
894                        " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else ""
895                    )),
896                ])
897
898            if "lot" in iJSON.keys():
899                info.append("| Minimum lot to buy:                                         | {:<54} |\n".format(iJSON["lot"]))
900
901            if "step" in iJSON.keys() and iJSON["step"] != 0:
902                info.append("| Minimum price increment (step):                             | {:<54} |\n".format("{} {}".format(iJSON["step"], iJSON["currency"] if "currency" in iJSON.keys() else "")))
903
904            # Add bond payment calendar:
905            if iJSON["type"] == "Bonds":
906                strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False)   # bond payment calendar
907                info.extend(["\n#", strCalendar])
908
909            infoText += "".join(info)
910
911            if show and not onlyFiles:
912                uLogger.info("{}".format(infoText))
913
914            if self.infoFile is not None and (show or onlyFiles):
915                with open(self.infoFile, "w", encoding="UTF-8") as fH:
916                    fH.write(infoText)
917
918                uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile)))
919
920                if self.useHTMLReports:
921                    htmlFilePath = self.infoFile.replace(".md", ".html") if self.infoFile.endswith(".md") else self.infoFile + ".html"
922                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
923                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Main information", commonCSS=COMMON_CSS, markdown=infoText))
924
925                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
926
927        return infoText

Show information about one instrument defined by json data and prints it in Markdown format.

See also: SearchByTicker(), SearchByFIGI(), RequestBondCoupons(), ExtendBondsData(), ShowBondsCalendar() and RequestTradingStatus().

Parameters
  • iJSON: json data of instrument, example: iJSON = self.iList["Shares"][self._ticker]
  • show: if True then also printing information about instrument and its current price.
  • onlyFiles: if True then do not show Markdown table in the console, but only generates report files.
Returns

multilines text in Markdown format with information about one instrument.

def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict:
929    def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict:
930        """
931        Search and return raw broker's information about instrument by its ticker. Variable `ticker` must be defined!
932
933        :param requestPrice: if `False` then do not request current price of instrument (because this is long operation).
934        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
935        :return: JSON formatted data with information about instrument.
936        """
937        tickerJSON = {}
938        if self.moreDebug:
939            uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self._ticker))
940
941        if not self._ticker:
942            uLogger.warning("self._ticker variable is not be empty!")
943
944        else:
945            if self._ticker in TKS_TICKERS_OR_FIGI_EXCLUDED:
946                uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self._ticker))
947                raise Exception("Instrument not allowed")
948
949            if not self.iList:
950                self.iList = self.Listing()
951
952            if self._ticker in self.iList["Shares"].keys():
953                tickerJSON = self.iList["Shares"][self._ticker]
954                if self.moreDebug:
955                    uLogger.debug("Ticker [{}] found in shares list".format(self._ticker))
956
957            elif self._ticker in self.iList["Currencies"].keys():
958                tickerJSON = self.iList["Currencies"][self._ticker]
959                if self.moreDebug:
960                    uLogger.debug("Ticker [{}] found in currencies list".format(self._ticker))
961
962            elif self._ticker in self.iList["Bonds"].keys():
963                tickerJSON = self.iList["Bonds"][self._ticker]
964                if self.moreDebug:
965                    uLogger.debug("Ticker [{}] found in bonds list".format(self._ticker))
966
967            elif self._ticker in self.iList["Etfs"].keys():
968                tickerJSON = self.iList["Etfs"][self._ticker]
969                if self.moreDebug:
970                    uLogger.debug("Ticker [{}] found in etfs list".format(self._ticker))
971
972            elif self._ticker in self.iList["Futures"].keys():
973                tickerJSON = self.iList["Futures"][self._ticker]
974                if self.moreDebug:
975                    uLogger.debug("Ticker [{}] found in futures list".format(self._ticker))
976
977        if tickerJSON:
978            self._figi = tickerJSON["figi"]
979
980            if requestPrice:
981                tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False)
982
983                if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None:
984                    tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"]
985
986                else:
987                    tickerJSON["currentPrice"]["changes"] = 0
988
989            if show:
990                self.ShowInstrumentInfo(iJSON=tickerJSON, show=True)  # print info as Markdown text
991
992        else:
993            if show:
994                uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self._ticker))
995
996        return tickerJSON

Search and return raw broker's information about instrument by its ticker. Variable ticker must be defined!

Parameters
  • requestPrice: if False then do not request current price of instrument (because this is long operation).
  • show: if False then do not run ShowInstrumentInfo() method and do not print info to the console.
Returns

JSON formatted data with information about instrument.

def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict:
 998    def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict:
 999        """
1000        Search and return raw broker's information about instrument by its FIGI. Variable `figi` must be defined!
1001
1002        :param requestPrice: if `False` then do not request current price of instrument (it's long operation).
1003        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
1004        :return: JSON formatted data with information about instrument.
1005        """
1006        figiJSON = {}
1007        if self.moreDebug:
1008            uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self._figi))
1009
1010        if not self._figi:
1011            uLogger.warning("self._figi variable is not be empty!")
1012
1013        else:
1014            if self._figi in TKS_TICKERS_OR_FIGI_EXCLUDED:
1015                uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self._figi))
1016                raise Exception("Instrument not allowed")
1017
1018            if not self.iList:
1019                self.iList = self.Listing()
1020
1021            for item in self.iList["Shares"].keys():
1022                if self._figi == self.iList["Shares"][item]["figi"]:
1023                    figiJSON = self.iList["Shares"][item]
1024
1025                    if self.moreDebug:
1026                        uLogger.debug("FIGI [{}] found in shares list".format(self._figi))
1027
1028                    break
1029
1030            if not figiJSON:
1031                for item in self.iList["Currencies"].keys():
1032                    if self._figi == self.iList["Currencies"][item]["figi"]:
1033                        figiJSON = self.iList["Currencies"][item]
1034
1035                        if self.moreDebug:
1036                            uLogger.debug("FIGI [{}] found in currencies list".format(self._figi))
1037
1038                        break
1039
1040            if not figiJSON:
1041                for item in self.iList["Bonds"].keys():
1042                    if self._figi == self.iList["Bonds"][item]["figi"]:
1043                        figiJSON = self.iList["Bonds"][item]
1044
1045                        if self.moreDebug:
1046                            uLogger.debug("FIGI [{}] found in bonds list".format(self._figi))
1047
1048                        break
1049
1050            if not figiJSON:
1051                for item in self.iList["Etfs"].keys():
1052                    if self._figi == self.iList["Etfs"][item]["figi"]:
1053                        figiJSON = self.iList["Etfs"][item]
1054
1055                        if self.moreDebug:
1056                            uLogger.debug("FIGI [{}] found in etfs list".format(self._figi))
1057
1058                        break
1059
1060            if not figiJSON:
1061                for item in self.iList["Futures"].keys():
1062                    if self._figi == self.iList["Futures"][item]["figi"]:
1063                        figiJSON = self.iList["Futures"][item]
1064
1065                        if self.moreDebug:
1066                            uLogger.debug("FIGI [{}] found in futures list".format(self._figi))
1067
1068                        break
1069
1070        if figiJSON:
1071            self._figi = figiJSON["figi"]
1072            self._ticker = figiJSON["ticker"]
1073
1074            if requestPrice:
1075                figiJSON["currentPrice"] = self.GetCurrentPrices(show=False)
1076
1077                if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None:
1078                    figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"]
1079
1080                else:
1081                    figiJSON["currentPrice"]["changes"] = 0
1082
1083            if show:
1084                self.ShowInstrumentInfo(iJSON=figiJSON, show=True)  # print info as Markdown text
1085
1086        else:
1087            if show:
1088                uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self._figi))
1089
1090        return figiJSON

Search and return raw broker's information about instrument by its FIGI. Variable figi must be defined!

Parameters
  • requestPrice: if False then do not request current price of instrument (it's long operation).
  • show: if False then do not run ShowInstrumentInfo() method and do not print info to the console.
Returns

JSON formatted data with information about instrument.

def GetCurrentPrices(self, show: bool = True) -> dict:
1092    def GetCurrentPrices(self, show: bool = True) -> dict:
1093        """
1094        Get and show Depth of Market with current prices of the instrument as dictionary. Result example with `depth` 5:
1095        `{"buy": [{"price": 1243.8, "quantity": 193},
1096                  {"price": 1244.0, "quantity": 168},
1097                  {"price": 1244.8, "quantity": 5},
1098                  {"price": 1245.0, "quantity": 61},
1099                  {"price": 1245.4, "quantity": 60}],
1100          "sell": [{"price": 1243.6, "quantity": 8},
1101                   {"price": 1242.6, "quantity": 10},
1102                   {"price": 1242.4, "quantity": 18},
1103                   {"price": 1242.2, "quantity": 50},
1104                   {"price": 1242.0, "quantity": 113}],
1105          "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}`, where parameters mean:
1106        - buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order
1107        - sell: list of dicts with Buyers prices,
1108            - price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument),
1109            - quantity: volume value by current price in lots,
1110        - limitUp: current trade session limit price, maximum,
1111        - limitDown: current trade session limit price, minimum,
1112        - lastPrice: last deal price of the instrument,
1113        - closePrice: previous trade session close price of the instrument.
1114
1115        See also: `SearchByTicker()` and `SearchByFIGI()`.
1116        REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1117        Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
1118
1119        :param show: if `True` then print DOM to log and console.
1120        :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`.
1121                 If an error occurred then returns an empty record:
1122                 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`.
1123        """
1124        prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0}
1125
1126        if self.depth < 1:
1127            uLogger.error("Depth of Market (DOM) must be >=1!")
1128            raise Exception("Incorrect value")
1129
1130        if not (self._ticker or self._figi):
1131            uLogger.error("self._ticker or self._figi variables must be defined!")
1132            raise Exception("Ticker or FIGI required")
1133
1134        if self._ticker and not self._figi:
1135            instrumentByTicker = self.SearchByTicker(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1136            self._figi = instrumentByTicker["figi"] if instrumentByTicker else ""
1137
1138        if not self._ticker and self._figi:
1139            instrumentByFigi = self.SearchByFIGI(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1140            self._ticker = instrumentByFigi["ticker"] if instrumentByFigi else ""
1141
1142        if not self._figi:
1143            uLogger.error("FIGI is not defined!")
1144            raise Exception("Ticker or FIGI required")
1145
1146        else:
1147            uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self._ticker, self._figi))
1148
1149            # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1150            priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook"
1151            self.body = str({"figi": self._figi, "depth": self.depth})
1152            pricesResponse = self.SendAPIRequest(priceURL, reqType="POST")  # Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
1153
1154            if pricesResponse and not ("code" in pricesResponse.keys() or "message" in pricesResponse.keys() or "description" in pricesResponse.keys()):
1155                # list of dicts with sellers orders:
1156                prices["buy"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]]
1157
1158                # list of dicts with buyers orders:
1159                prices["sell"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]]
1160
1161                # max price of instrument at this time:
1162                prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None
1163
1164                # min price of instrument at this time:
1165                prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None
1166
1167                # last price of deal with instrument:
1168                prices["lastPrice"] = round(NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]), 6) if "lastPrice" in pricesResponse.keys() else 0
1169
1170                # last close price of instrument:
1171                prices["closePrice"] = round(NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]), 6) if "closePrice" in pricesResponse.keys() else 0
1172
1173            else:
1174                uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi))
1175                uLogger.debug("Server response: {}".format(pricesResponse))
1176
1177            if show:
1178                if prices["buy"] or prices["sell"]:
1179                    info = [
1180                        "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format(
1181                            datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
1182                            self._ticker,
1183                            self._figi,
1184                            self.depth,
1185                        ),
1186                        "-" * 60, "\n",
1187                        "             Orders of Buyers | Orders of Sellers\n",
1188                        "-" * 60, "\n",
1189                        "        Sell prices (volumes) | Buy prices (volumes)\n",
1190                        "-" * 60, "\n",
1191                    ]
1192
1193                    if not prices["buy"]:
1194                        info.append("                              | No orders!\n")
1195                        sumBuy = 0
1196
1197                    else:
1198                        sumBuy = sum([x["quantity"] for x in prices["buy"]])
1199                        maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True)
1200                        for item in maxMinSorted:
1201                            info.append("                              | {} ({})\n".format(item["price"], item["quantity"]))
1202
1203                    if not prices["sell"]:
1204                        info.append("No orders!                    |\n")
1205                        sumSell = 0
1206
1207                    else:
1208                        sumSell = sum([x["quantity"] for x in prices["sell"]])
1209                        for item in prices["sell"]:
1210                            info.append("{:>29} |\n".format("{} ({})".format(item["price"], item["quantity"])))
1211
1212                    info.extend([
1213                        "-" * 60, "\n",
1214                        "{:>29} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)),
1215                        "-" * 60, "\n",
1216                    ])
1217
1218                    infoText = "".join(info)
1219
1220                    uLogger.info("Current prices in order book:\n\n{}".format(infoText))
1221
1222                else:
1223                    uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi))
1224
1225        return prices

Get and show Depth of Market with current prices of the instrument as dictionary. Result example with depth 5: {"buy": [{"price": 1243.8, "quantity": 193}, {"price": 1244.0, "quantity": 168}, {"price": 1244.8, "quantity": 5}, {"price": 1245.0, "quantity": 61}, {"price": 1245.4, "quantity": 60}], "sell": [{"price": 1243.6, "quantity": 8}, {"price": 1242.6, "quantity": 10}, {"price": 1242.4, "quantity": 18}, {"price": 1242.2, "quantity": 50}, {"price": 1242.0, "quantity": 113}], "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}, where parameters mean:

  • buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order
  • sell: list of dicts with Buyers prices,
    • price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument),
    • quantity: volume value by current price in lots,
  • limitUp: current trade session limit price, maximum,
  • limitDown: current trade session limit price, minimum,
  • lastPrice: last deal price of the instrument,
  • closePrice: previous trade session close price of the instrument.

See also: SearchByTicker() and SearchByFIGI(). REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse

Parameters
  • show: if True then print DOM to log and console.
Returns

orders book dict with lists of current buy and sell prices: {"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}. If an error occurred then returns an empty record: {"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}.

def ShowInstrumentsInfo(self, show: bool = True, onlyFiles=False) -> str:
1227    def ShowInstrumentsInfo(self, show: bool = True, onlyFiles=False) -> str:
1228        """
1229        This method get and show information about all available broker instruments for current user account.
1230        If `instrumentsFile` string is not empty then also save information to this file.
1231
1232        :param show: if `True` then print results to console, if `False` — print only to file.
1233        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
1234        :return: multi-lines string with all available broker instruments.
1235        """
1236        if not self.iList:
1237            self.iList = self.Listing()
1238
1239        info = [
1240            "# All available instruments from Tinkoff Broker server for current user token\n\n",
1241            "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
1242        ]
1243
1244        # add instruments count by type:
1245        for iType in self.iList.keys():
1246            info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType])))
1247
1248        headerLine = "| Ticker       | Full name                                                 | FIGI         | Cur | Lot     | Step       |\n"
1249        splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n"
1250
1251        # generating info tables with all instruments by type:
1252        for iType in self.iList.keys():
1253            info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine])
1254
1255            for instrument in self.iList[iType].keys():
1256                iName = self.iList[iType][instrument]["name"]  # instrument's name
1257                if len(iName) > 57:
1258                    iName = "{}...".format(iName[:54])  # right trim for a long string
1259
1260                info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format(
1261                    self.iList[iType][instrument]["ticker"],
1262                    iName,
1263                    self.iList[iType][instrument]["figi"],
1264                    self.iList[iType][instrument]["currency"],
1265                    self.iList[iType][instrument]["lot"],
1266                    "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0,
1267                ))
1268
1269        infoText = "".join(info)
1270
1271        if show and not onlyFiles:
1272            uLogger.info(infoText)
1273
1274        if self.instrumentsFile and (show or onlyFiles):
1275            with open(self.instrumentsFile, "w", encoding="UTF-8") as fH:
1276                fH.write(infoText)
1277
1278            uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile)))
1279
1280            if self.useHTMLReports:
1281                htmlFilePath = self.instrumentsFile.replace(".md", ".html") if self.instrumentsFile.endswith(".md") else self.instrumentsFile + ".html"
1282                with open(htmlFilePath, "w", encoding="UTF-8") as fH:
1283                    fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="List of instruments", commonCSS=COMMON_CSS, markdown=infoText))
1284
1285                uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
1286
1287        return infoText

This method get and show information about all available broker instruments for current user account. If instrumentsFile string is not empty then also save information to this file.

Parameters
  • show: if True then print results to console, if False — print only to file.
  • onlyFiles: if True then do not show Markdown table in the console, but only generates report files.
Returns

multi-lines string with all available broker instruments.

def SearchInstruments(self, pattern: str, show: bool = True, onlyFiles=False) -> dict:
1289    def SearchInstruments(self, pattern: str, show: bool = True, onlyFiles=False) -> dict:
1290        """
1291        This method search and show information about instruments by part of its ticker, FIGI or name.
1292        If `searchResultsFile` string is not empty then also save information to this file.
1293
1294        :param pattern: string with part of ticker, FIGI or instrument's name.
1295        :param show: if `True` then print results to console, if `False` — return list of result only.
1296        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
1297        :return: list of dictionaries with all found instruments.
1298        """
1299        if not self.iList:
1300            self.iList = self.Listing()
1301
1302        searchResults = {iType: {} for iType in self.iList}  # same as iList but will contain only filtered instruments
1303        compiledPattern = re.compile(pattern, re.IGNORECASE)
1304
1305        for iType in self.iList:
1306            for instrument in self.iList[iType].values():
1307                searchResult = compiledPattern.search(" ".join(
1308                    [instrument["ticker"], instrument["figi"], instrument["name"]]
1309                ))
1310
1311                if searchResult:
1312                    searchResults[iType][instrument["ticker"]] = instrument
1313
1314        resultsLen = sum([len(searchResults[iType]) for iType in searchResults])
1315        info = [
1316            "# Search results\n\n",
1317            "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
1318            "* **Search pattern:** [{}]\n".format(pattern),
1319            "* **Found instruments:** [{}]\n\n".format(resultsLen),
1320            '**Note:** you can view info about found instruments with key "--info", e.g.: "tksbrokerapi -t TICKER --info" or "tksbrokerapi -f FIGI --info".\n'
1321        ]
1322        infoShort = info[:]
1323
1324        headerLine = "| Type       | Ticker       | Full name                                                      | FIGI         |\n"
1325        splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n"
1326        skippedLine = "| ...        | ...          | ...                                                            | ...          |\n"
1327
1328        if resultsLen == 0:
1329            info.append("\nNo results\n")
1330            infoShort.append("\nNo results\n")
1331            uLogger.warning("No results. Try changing your search pattern.")
1332
1333        else:
1334            for iType in searchResults:
1335                iTypeValuesCount = len(searchResults[iType].values())
1336                if iTypeValuesCount > 0:
1337                    info.extend(["\n## {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1338                    infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1339
1340                    for instrument in searchResults[iType].values():
1341                        info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format(
1342                            instrument["type"],
1343                            instrument["ticker"],
1344                            "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"],  # right trim for a long string
1345                            instrument["figi"],
1346                        ))
1347
1348                    if iTypeValuesCount <= 5:
1349                        infoShort.extend(info[-iTypeValuesCount:])
1350
1351                    else:
1352                        infoShort.extend(info[-5:])
1353                        infoShort.append(skippedLine)
1354
1355        infoText = "".join(info)
1356        infoTextShort = "".join(infoShort)
1357
1358        if show and not onlyFiles:
1359            uLogger.info(infoTextShort)
1360            uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`")
1361
1362        if self.searchResultsFile and (show or onlyFiles):
1363            with open(self.searchResultsFile, "w", encoding="UTF-8") as fH:
1364                fH.write(infoText)
1365
1366            uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile)))
1367
1368            if self.useHTMLReports:
1369                htmlFilePath = self.searchResultsFile.replace(".md", ".html") if self.searchResultsFile.endswith(".md") else self.searchResultsFile + ".html"
1370                with open(htmlFilePath, "w", encoding="UTF-8") as fH:
1371                    fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Search results", commonCSS=COMMON_CSS, markdown=infoText))
1372
1373                uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
1374
1375        return searchResults

This method search and show information about instruments by part of its ticker, FIGI or name. If searchResultsFile string is not empty then also save information to this file.

Parameters
  • pattern: string with part of ticker, FIGI or instrument's name.
  • show: if True then print results to console, if False — return list of result only.
  • onlyFiles: if True then do not show Markdown table in the console, but only generates report files.
Returns

list of dictionaries with all found instruments.

def GetUniqueFIGIs(self, instruments: list[str]) -> list:
1377    def GetUniqueFIGIs(self, instruments: list[str]) -> list:
1378        """
1379        Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs.
1380
1381        :param instruments: list of strings with tickers or FIGIs.
1382        :return: list with unique instrument FIGIs only.
1383        """
1384        requestedInstruments = []
1385        for iName in instruments:
1386            if iName not in self.aliases.keys():
1387                if iName not in requestedInstruments:
1388                    requestedInstruments.append(iName)
1389
1390            else:
1391                if iName not in requestedInstruments:
1392                    if self.aliases[iName] not in requestedInstruments:
1393                        requestedInstruments.append(self.aliases[iName])
1394
1395        uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments))
1396
1397        onlyUniqueFIGIs = []
1398        for iName in requestedInstruments:
1399            if iName in TKS_TICKERS_OR_FIGI_EXCLUDED:
1400                continue
1401
1402            self._ticker = iName
1403            iData = self.SearchByTicker(requestPrice=False)  # trying to find instrument by ticker
1404
1405            if not iData:
1406                self._ticker = ""
1407                self._figi = iName
1408
1409                iData = self.SearchByFIGI(requestPrice=False)  # trying to find instrument by FIGI
1410
1411                if not iData:
1412                    self._figi = ""
1413                    uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName))
1414
1415            if iData and iData["figi"] not in onlyUniqueFIGIs:
1416                onlyUniqueFIGIs.append(iData["figi"])
1417
1418        uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs))
1419
1420        return onlyUniqueFIGIs

Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs.

Parameters
  • instruments: list of strings with tickers or FIGIs.
Returns

list with unique instrument FIGIs only.

def GetListOfPrices( self, instruments: list[str], show: bool = False, onlyFiles=False) -> list[dict]:
1422    def GetListOfPrices(self, instruments: list[str], show: bool = False, onlyFiles=False) -> list[dict]:
1423        """
1424        This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation!
1425
1426        See limits: https://tinkoff.github.io/investAPI/limits/
1427
1428        If `pricesFile` string is not empty then also save information to this file.
1429
1430        :param instruments: list of strings with tickers or FIGIs.
1431        :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`.
1432        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
1433        :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1434                 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods.
1435        """
1436        if instruments is None or not instruments:
1437            uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!")
1438            raise Exception("Ticker or FIGI required")
1439
1440        onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments)
1441
1442        uLogger.debug("Requesting current prices from Tinkoff Broker server...")
1443
1444        iList = []  # trying to get info and current prices about all unique instruments:
1445        for self._figi in onlyUniqueFIGIs:
1446            iData = self.SearchByFIGI(requestPrice=True, show=False)
1447            iList.append(iData)
1448
1449        self.ShowListOfPrices(iList, show, onlyFiles)
1450
1451        return iList

This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation!

See limits: https://tinkoff.github.io/investAPI/limits/

If pricesFile string is not empty then also save information to this file.

Parameters
  • instruments: list of strings with tickers or FIGIs.
  • show: if True then prints prices to console, if False — prints only to file pricesFile.
  • onlyFiles: if True then do not show Markdown table in the console, but only generates report files.
Returns

list of instruments looks like [{some ticker info, "currentPrice": {current prices}}, {...}, ...]. One item is dict returned by SearchByTicker() or SearchByFIGI() methods.

def ShowListOfPrices(self, iList: list, show: bool = True, onlyFiles=False) -> str:
1453    def ShowListOfPrices(self, iList: list, show: bool = True, onlyFiles=False) -> str:
1454        """
1455        Show table contains current prices of given instruments.
1456
1457        :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1458                      One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods.
1459        :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`.
1460        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
1461        :return: multilines text in Markdown format as a table contains current prices.
1462        """
1463        infoText = ""
1464
1465        if show or self.pricesFile or onlyFiles:
1466            info = [
1467                "# Current prices\n\n* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
1468                "| Ticker       | FIGI         | Type       | Prev. close | Last price  | Chg. %   | Day limits min/max  | Actual sell / buy   | Curr. |\n",
1469                "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n",
1470            ]
1471
1472            for item in iList:
1473                info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format(
1474                    item["ticker"],
1475                    item["figi"],
1476                    item["type"],
1477                    "{:.2f}".format(float(item["currentPrice"]["closePrice"])),
1478                    "{:.2f}".format(float(item["currentPrice"]["lastPrice"])),
1479                    "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])),
1480                    "{} / {}".format(
1481                        item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A",
1482                        item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A",
1483                    ),
1484                    "{} / {}".format(
1485                        item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A",
1486                        item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A",
1487                    ),
1488                    item["currency"],
1489                ))
1490
1491            infoText = "".join(info)
1492
1493            if show and not onlyFiles:
1494                uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText))
1495
1496            if self.pricesFile and (show or onlyFiles):
1497                with open(self.pricesFile, "w", encoding="UTF-8") as fH:
1498                    fH.write(infoText)
1499
1500                uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile)))
1501
1502                if self.useHTMLReports:
1503                    htmlFilePath = self.pricesFile.replace(".md", ".html") if self.pricesFile.endswith(".md") else self.pricesFile + ".html"
1504                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
1505                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Current prices", commonCSS=COMMON_CSS, markdown=infoText))
1506
1507                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
1508
1509        return infoText

Show table contains current prices of given instruments.

Parameters
  • **iList: list of instruments looks like [{some ticker info, "currentPrice"**: {current prices}}, {...}, ...]. One item is dict returned by SearchByTicker(requestPrice=True) or by SearchByFIGI(requestPrice=True) methods.
  • show: if True then prints prices to console, if False — prints only to file pricesFile.
  • onlyFiles: if True then do not show Markdown table in the console, but only generates report files.
Returns

multilines text in Markdown format as a table contains current prices.

def RequestTradingStatus(self) -> dict:
1511    def RequestTradingStatus(self) -> dict:
1512        """
1513        Requesting trading status for the instrument defined by `figi` variable.
1514
1515        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus
1516
1517        Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest
1518
1519        :return: dictionary with trading status attributes. Response example:
1520                 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING",
1521                  "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}`
1522        """
1523        if self._figi is None or not self._figi:
1524            uLogger.error("Variable `figi` must be defined for using this method!")
1525            raise Exception("FIGI required")
1526
1527        uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self._figi))
1528
1529        self.body = str({"figi": self._figi, "instrumentId": self._figi})
1530        tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus"
1531        tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST")
1532
1533        if self.moreDebug:
1534            uLogger.debug("Records about current trading status successfully received")
1535
1536        return tradingStatus

Requesting trading status for the instrument defined by figi variable.

REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus

Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest

Returns

dictionary with trading status attributes. Response example: {"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING", "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}

def RequestPortfolio(self) -> dict:
1538    def RequestPortfolio(self) -> dict:
1539        """
1540        Requesting actual user's portfolio for current `accountId`.
1541
1542        REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio
1543
1544        Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest
1545
1546        :return: dictionary with user's portfolio.
1547        """
1548        if self.accountId is None or not self.accountId:
1549            uLogger.error("Variable `accountId` must be defined for using this method!")
1550            raise Exception("Account ID required")
1551
1552        uLogger.debug("Requesting current actual user's portfolio. Wait, please...")
1553
1554        self.body = str({"accountId": self.accountId})
1555        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio"
1556        rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST")
1557
1558        if self.moreDebug:
1559            uLogger.debug("Records about user's portfolio successfully received")
1560
1561        return rawPortfolio

Requesting actual user's portfolio for current accountId.

REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio

Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest

Returns

dictionary with user's portfolio.

def RequestPositions(self) -> dict:
1563    def RequestPositions(self) -> dict:
1564        """
1565        Requesting open positions by currencies and instruments for current `accountId`.
1566
1567        REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions
1568
1569        Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest
1570
1571        :return: dictionary with open positions by instruments.
1572        """
1573        if self.accountId is None or not self.accountId:
1574            uLogger.error("Variable `accountId` must be defined for using this method!")
1575            raise Exception("Account ID required")
1576
1577        uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...")
1578
1579        self.body = str({"accountId": self.accountId})
1580        positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions"
1581        rawPositions = self.SendAPIRequest(positionsURL, reqType="POST")
1582
1583        if self.moreDebug:
1584            uLogger.debug("Records about current open positions successfully received")
1585
1586        return rawPositions

Requesting open positions by currencies and instruments for current accountId.

REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions

Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest

Returns

dictionary with open positions by instruments.

def RequestPendingOrders(self) -> list:
1588    def RequestPendingOrders(self) -> list:
1589        """
1590        Requesting current actual pending limit orders for current `accountId`.
1591
1592        REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders
1593
1594        Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest
1595
1596        :return: list of dictionaries with pending limit orders.
1597        """
1598        if self.accountId is None or not self.accountId:
1599            uLogger.error("Variable `accountId` must be defined for using this method!")
1600            raise Exception("Account ID required")
1601
1602        uLogger.debug("Requesting current actual pending limit orders. Wait, please...")
1603
1604        self.body = str({"accountId": self.accountId})
1605        ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders"
1606        rawResponse = self.SendAPIRequest(ordersURL, reqType="POST")
1607
1608        if "orders" in rawResponse.keys():
1609            rawOrders = rawResponse["orders"]
1610            uLogger.debug("[{}] records about pending limit orders received".format(len(rawOrders)))
1611
1612        else:
1613            rawOrders = []
1614            uLogger.debug("No pending limit orders returned! rawResponse = {}".format(rawResponse))
1615
1616        return rawOrders

Requesting current actual pending limit orders for current accountId.

REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders

Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest

Returns

list of dictionaries with pending limit orders.

def RequestStopOrders(self) -> list:
1618    def RequestStopOrders(self) -> list:
1619        """
1620        Requesting current actual stop orders for current `accountId`.
1621
1622        REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders
1623
1624        Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest
1625
1626        :return: list of dictionaries with stop orders.
1627        """
1628        if self.accountId is None or not self.accountId:
1629            uLogger.error("Variable `accountId` must be defined for using this method!")
1630            raise Exception("Account ID required")
1631
1632        uLogger.debug("Requesting current actual stop orders. Wait, please...")
1633
1634        self.body = str({"accountId": self.accountId})
1635        stopOrdersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders"
1636        rawResponse = self.SendAPIRequest(stopOrdersURL, reqType="POST")
1637
1638        if "stopOrders" in rawResponse.keys():
1639            rawStopOrders = rawResponse["stopOrders"]
1640            uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders)))
1641
1642        else:
1643            rawStopOrders = []
1644            uLogger.debug("No stop orders returned! rawResponse = {}".format(rawResponse))
1645
1646        return rawStopOrders

Requesting current actual stop orders for current accountId.

REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders

Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest

Returns

list of dictionaries with stop orders.

def Overview(self, show: bool = False, details: str = 'full', onlyFiles=False) -> dict:
1648    def Overview(self, show: bool = False, details: str = "full", onlyFiles=False) -> dict:
1649        """
1650        Get portfolio: all open positions, orders and some statistics for current `accountId`.
1651        If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile`
1652        and `overviewBondsCalendarFile` are defined then also save information to file.
1653
1654        WARNING! It is not recommended to run this method too many times in a loop! The server receives
1655        many requests about the state of the portfolio, and then, based on the received data, a large number
1656        of calculation and statistics are collected.
1657
1658        :param show: if `False` then only dictionary returns, if `True` then show more debug information.
1659        :param details: how detailed should the information be?
1660        - `full` — shows full available information about portfolio status (by default),
1661        - `positions` — shows only open positions,
1662        - `orders` — shows only sections of open limits and stop orders.
1663        - `digest` — show a short digest of the portfolio status,
1664        - `analytics` — shows only the analytics section and the distribution of the portfolio by various categories,
1665        - `calendar` — shows only the bonds calendar section (if these present in portfolio).
1666        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
1667        :return: dictionary with client's raw portfolio and some statistics.
1668        """
1669        if self.accountId is None or not self.accountId:
1670            uLogger.error("Variable `accountId` must be defined for using this method!")
1671            raise Exception("Account ID required")
1672
1673        view = {
1674            "raw": {  # --- raw portfolio responses from broker with user portfolio data:
1675                "headers": {},  # list of dictionaries, response headers without "positions" section
1676                "Currencies": [],  # list of dictionaries, open trades with currencies from "positions" section
1677                "Shares": [],  # list of dictionaries, open trades with shares from "positions" section
1678                "Bonds": [],  # list of dictionaries, open trades with bonds from "positions" section
1679                "Etfs": [],  # list of dictionaries, open trades with etfs from "positions" section
1680                "Futures": [],  # list of dictionaries, open trades with futures from "positions" section
1681                "positions": {},  # raw response from broker: dictionary with current available or blocked currencies and instruments for client
1682                "orders": [],  # raw response from broker: list of dictionaries with all pending (market) orders
1683                "stopOrders": [],  # raw response from broker: list of dictionaries with all stop orders
1684                "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}},  # dict with prices of all currencies in RUB
1685            },
1686            "stat": {  # --- some statistics calculated using "raw" sections:
1687                "portfolioCostRUB": 0.,  # portfolio cost in RUB (Russian Rouble)
1688                "availableRUB": 0.,  # available rubles (without other currencies)
1689                "blockedRUB": 0.,  # blocked sum in Russian Rouble
1690                "totalChangesRUB": 0.,  # changes for all open trades in RUB
1691                "totalChangesPercentRUB": 0.,  # changes for all open trades in percents
1692                "allCurrenciesCostRUB": 0.,  # costs of all currencies (include rubles) in RUB
1693                "sharesCostRUB": 0.,  # costs of all shares in RUB
1694                "bondsCostRUB": 0.,  # costs of all bonds in RUB
1695                "etfsCostRUB": 0.,  # costs of all etfs in RUB
1696                "futuresCostRUB": 0.,  # costs of all futures in RUB
1697                "Currencies": [],  # list of dictionaries of all currencies statistics
1698                "Shares": [],  # list of dictionaries of all shares statistics
1699                "Bonds": [],  # list of dictionaries of all bonds statistics
1700                "Etfs": [],  # list of dictionaries of all etfs statistics
1701                "Futures": [],  # list of dictionaries of all futures statistics
1702                "orders": [],  # list of dictionaries of all pending (market) orders and it's parameters
1703                "stopOrders": [],  # list of dictionaries of all stop orders and it's parameters
1704                "blockedCurrencies": {},  # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21}
1705                "blockedInstruments": {},  # dict with blocked  by FIGI, e.g. {}
1706                "funds": {},  # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1707            },
1708            "analytics": {  # --- some analytics of portfolio:
1709                "distrByAssets": {},  # portfolio distribution by assets
1710                "distrByCompanies": {},  # portfolio distribution by companies
1711                "distrBySectors": {},  # portfolio distribution by sectors
1712                "distrByCurrencies": {},  # portfolio distribution by currencies
1713                "distrByCountries": {},  # portfolio distribution by countries
1714                "bondsCalendar": None,  # bonds payment calendar as Pandas DataFrame (if these present in portfolio)
1715            }
1716        }
1717
1718        details = details.lower()
1719        availableDetails = ["full", "positions", "orders", "analytics", "calendar", "digest"]
1720        if details not in availableDetails:
1721            details = "full"
1722            uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails))
1723
1724        uLogger.debug("Requesting portfolio of a client. Wait, please...")
1725
1726        portfolioResponse = self.RequestPortfolio()  # current user's portfolio (dict)
1727        view["raw"]["positions"] = self.RequestPositions()  # current open positions by instruments (dict)
1728        view["raw"]["orders"] = self.RequestPendingOrders()  # current actual pending limit orders (list)
1729        view["raw"]["stopOrders"] = self.RequestStopOrders()  # current actual stop orders (list)
1730
1731        # save response headers without "positions" section:
1732        for key in portfolioResponse.keys():
1733            if key != "positions":
1734                view["raw"]["headers"][key] = portfolioResponse[key]
1735
1736            else:
1737                continue
1738
1739        # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation
1740        # Type of instrument must be only one of supported types in TKS_INSTRUMENTS
1741        for item in portfolioResponse["positions"]:
1742            if item["instrumentType"] == "currency":
1743                self._figi = item["figi"]
1744                if not self._figi and item["ticker"]:
1745                    self._ticker = item["ticker"]
1746                    self._figi = self.SearchByTicker()["figi"]  # Get FIGI to avoid warnings
1747
1748                curr = self.SearchByFIGI(requestPrice=False)
1749
1750                # current price of currency in RUB:
1751                view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = {
1752                    "name": curr["name"],
1753                    "currentPrice": NanoToFloat(
1754                        item["currentPrice"]["units"],
1755                        item["currentPrice"]["nano"]
1756                    ),
1757                }
1758
1759                view["raw"]["Currencies"].append(item)
1760
1761            elif item["instrumentType"] == "share":
1762                view["raw"]["Shares"].append(item)
1763
1764            elif item["instrumentType"] == "bond":
1765                view["raw"]["Bonds"].append(item)
1766
1767            elif item["instrumentType"] == "etf":
1768                view["raw"]["Etfs"].append(item)
1769
1770            elif item["instrumentType"] == "futures":
1771                view["raw"]["Futures"].append(item)
1772
1773            else:
1774                continue
1775
1776        # how many volume of currencies (by ISO currency name) are blocked:
1777        for item in view["raw"]["positions"]["blocked"]:
1778            blocked = NanoToFloat(item["units"], item["nano"])
1779            if blocked > 0:
1780                view["stat"]["blockedCurrencies"][item["currency"]] = blocked
1781
1782        # how many volume of instruments (by FIGI) are blocked:
1783        for item in view["raw"]["positions"]["securities"]:
1784            blocked = int(item["blocked"])
1785            if blocked > 0:
1786                view["stat"]["blockedInstruments"][item["figi"]] = blocked
1787
1788        allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]}
1789
1790        if "rub" in allBlocked.keys():
1791            view["stat"]["blockedRUB"] = allBlocked["rub"]  # blocked rubles
1792
1793        # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies:
1794        view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"])
1795        view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"])
1796        view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"])
1797        view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"])
1798        view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"])
1799        view["stat"]["portfolioCostRUB"] = sum([
1800            view["stat"]["allCurrenciesCostRUB"],
1801            view["stat"]["sharesCostRUB"],
1802            view["stat"]["bondsCostRUB"],
1803            view["stat"]["etfsCostRUB"],
1804            view["stat"]["futuresCostRUB"],
1805        ])
1806
1807        # --- calculating some portfolio statistics:
1808        byComp = {}  # distribution by companies
1809        bySect = {}  # distribution by sectors
1810        byCurr = {}  # distribution by currencies (include RUB)
1811        unknownCountryName = "All other countries"  # default name for instruments without "countryOfRisk" and "countryOfRiskName"
1812        byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}}  # distribution by countries (currencies are included in their countries)
1813
1814        for item in portfolioResponse["positions"]:
1815            self._figi = item["figi"]
1816            if not self._figi and item["ticker"]:
1817                self._ticker = item["ticker"]
1818                self._figi = self.SearchByTicker()["figi"]  # Get FIGI to avoid warnings
1819
1820            instrument = self.SearchByFIGI(requestPrice=False)  # full raw info about instrument by FIGI
1821
1822            if instrument:
1823                if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys():
1824                    blocked = allBlocked[instrument["nominal"]["currency"]]  # blocked volume of currency
1825
1826                elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys():
1827                    blocked = allBlocked[item["figi"]]  # blocked volume of other instruments
1828
1829                else:
1830                    blocked = 0
1831
1832                volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"])  # available volume of instrument
1833                lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"])  # available volume in lots of instrument
1834                direction = "Long" if lots >= 0 else "Short"  # direction of an instrument's position: short or long
1835                curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"])  # current instrument's price
1836                average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"])  # current average position price
1837                profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"])  # expected profit at current moment
1838                currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"]  # currency name rub, usd, eur etc.
1839                cost = curPrice if "currentNkd" not in item.keys() else (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume  # current cost of all volume of instrument in basic asset
1840                baseCurrencyName = item["currentPrice"]["currency"]  # name of base currency (rub)
1841                countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName
1842                costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"]  # cost in rubles
1843                percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.  # instrument's part in percent of full portfolio cost
1844
1845                statData = {
1846                    "figi": item["figi"],  # FIGI from REST API "GetPortfolio" method
1847                    "ticker": instrument["ticker"],  # ticker by FIGI
1848                    "currency": currency,  # currency name rub, usd, eur etc. for instrument price
1849                    "volume": volume,  # available volume of instrument
1850                    "lots": lots,  # volume in lots of instrument
1851                    "direction": direction,  # direction of an instrument's position: short or long
1852                    "blocked": blocked,  # blocked volume of currency or instrument
1853                    "currentPrice": curPrice,  # current instrument's price in basic asset
1854                    "average": average,  # current average position price
1855                    "cost": cost,  # current cost of all volume of instrument in basic asset
1856                    "baseCurrencyName": baseCurrencyName,  # name of base currency (rub)
1857                    "costRUB": costRUB,  # cost of instrument in ruble
1858                    "percentCostRUB": percentCostRUB,  # instrument's part in percent of full portfolio cost in RUB
1859                    "profit": profit,  # expected profit at current moment
1860                    "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0,  # expected percents of profit at current moment for this instrument
1861                    "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other",
1862                    "name": instrument["name"] if "name" in instrument.keys() else "",  # human-readable names of instruments
1863                    "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "",  # ISO name for currencies only
1864                    "country": countryName,  # e.g. "[RU] Российская Федерация" or unknownCountryName
1865                    "step": instrument["step"],  # minimum price increment
1866                }
1867
1868                # adding distribution by unique countries:
1869                if statData["country"] not in byCountry.keys():
1870                    byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB}
1871
1872                else:
1873                    byCountry[statData["country"]]["cost"] += costRUB
1874                    byCountry[statData["country"]]["percent"] += percentCostRUB
1875
1876                if item["instrumentType"] != "currency":
1877                    # adding distribution by unique companies:
1878                    if statData["name"]:
1879                        if statData["name"] not in byComp.keys():
1880                            byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB}
1881
1882                        else:
1883                            byComp[statData["name"]]["cost"] += costRUB
1884                            byComp[statData["name"]]["percent"] += percentCostRUB
1885
1886                    # adding distribution by unique sectors:
1887                    if statData["sector"] not in bySect.keys():
1888                        bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB}
1889
1890                    else:
1891                        bySect[statData["sector"]]["cost"] += costRUB
1892                        bySect[statData["sector"]]["percent"] += percentCostRUB
1893
1894                # adding distribution by unique currencies:
1895                if currency not in byCurr.keys():
1896                    byCurr[currency] = {
1897                        "name": view["raw"]["currenciesCurrentPrices"][currency]["name"],
1898                        "cost": costRUB,
1899                        "percent": percentCostRUB
1900                    }
1901
1902                else:
1903                    byCurr[currency]["cost"] += costRUB
1904                    byCurr[currency]["percent"] += percentCostRUB
1905
1906                # saving statistics for every instrument:
1907                if item["instrumentType"] == "currency":
1908                    view["stat"]["Currencies"].append(statData)
1909
1910                    # update dict with free funds for trading (total - blocked) by currencies
1911                    # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1912                    view["stat"]["funds"][currency] = {
1913                        "total": volume,
1914                        "totalCostRUB": costRUB,  # total volume cost in rubles
1915                        "free": volume - blocked,
1916                        "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0,  # free volume cost in rubles
1917                    }
1918
1919                elif item["instrumentType"] == "share":
1920                    view["stat"]["Shares"].append(statData)
1921
1922                elif item["instrumentType"] == "bond":
1923                    view["stat"]["Bonds"].append(statData)
1924
1925                elif item["instrumentType"] == "etf":
1926                    view["stat"]["Etfs"].append(statData)
1927
1928                elif item["instrumentType"] == "Futures":
1929                    view["stat"]["Futures"].append(statData)
1930
1931                else:
1932                    continue
1933
1934        # total changes in Russian Ruble:
1935        view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]])  # available RUB without other currencies
1936        view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0.
1937        startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100)
1938        view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost
1939        view["stat"]["funds"]["rub"] = {
1940            "total": view["stat"]["availableRUB"],
1941            "totalCostRUB": view["stat"]["availableRUB"],
1942            "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1943            "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1944        }
1945
1946        # --- pending limit orders sector data:
1947        uniquePendingOrdersFIGIs = []  # unique FIGIs of pending limit orders to avoid many times price requests
1948        uniquePendingOrders = {}  # unique instruments with FIGIs as dictionary keys
1949
1950        for item in view["raw"]["orders"]:
1951            self._figi = item["figi"]
1952
1953            if item["figi"] not in uniquePendingOrdersFIGIs:
1954                instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI, price requests only one time
1955
1956                uniquePendingOrdersFIGIs.append(item["figi"])
1957                uniquePendingOrders[item["figi"]] = instrument
1958
1959            else:
1960                instrument = uniquePendingOrders[item["figi"]]
1961
1962            if instrument:
1963                action = TKS_ORDER_DIRECTIONS[item["direction"]]
1964                orderType = TKS_ORDER_TYPES[item["orderType"]]
1965                orderState = TKS_ORDER_STATES[item["executionReportStatus"]]
1966                orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1967
1968                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1969                if item["direction"] == "ORDER_DIRECTION_BUY":
1970                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
1971
1972                else:
1973                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
1974
1975                # requested price for order execution:
1976                target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"])
1977
1978                # necessary changes in percent to reach target from current price:
1979                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
1980
1981                view["stat"]["orders"].append({
1982                    "orderID": item["orderId"],  # orderId number parameter of current order
1983                    "figi": item["figi"],  # FIGI identification
1984                    "ticker": instrument["ticker"],  # ticker name by FIGI
1985                    "lotsRequested": item["lotsRequested"],  # requested lots value
1986                    "lotsExecuted": item["lotsExecuted"],  # how many lots are executed
1987                    "currentPrice": lastPrice,  # current instrument's price for defined action
1988                    "targetPrice": target,  # requested price for order execution in base currency
1989                    "baseCurrencyName": item["initialSecurityPrice"]["currency"],  # name of base currency
1990                    "percentChanges": changes,  # changes in percent to target from current price
1991                    "currency": item["currency"],  # instrument's currency name
1992                    "action": action,  # sell / buy / Unknown from TKS_ORDER_DIRECTIONS
1993                    "type": orderType,  # type of order from TKS_ORDER_TYPES
1994                    "status": orderState,  # order status from TKS_ORDER_STATES
1995                    "date": orderDate,  # string with order date and time from UTC format (without nano seconds part)
1996                })
1997
1998        # --- stop orders sector data:
1999        uniqueStopOrdersFIGIs = []  # unique FIGIs of stop orders to avoid many times price requests
2000        uniqueStopOrders = {}  # unique instruments with FIGIs as dictionary keys
2001
2002        for item in view["raw"]["stopOrders"]:
2003            self._figi = item["figi"]
2004
2005            if item["figi"] not in uniqueStopOrdersFIGIs:
2006                instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI, price requests only one time
2007
2008                uniqueStopOrdersFIGIs.append(item["figi"])
2009                uniqueStopOrders[item["figi"]] = instrument
2010
2011            else:
2012                instrument = uniqueStopOrders[item["figi"]]
2013
2014            if instrument:
2015                action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]]
2016                orderType = TKS_STOP_ORDER_TYPES[item["orderType"]]
2017                createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
2018
2019                # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order
2020                if "expirationTime" in item.keys():
2021                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"]
2022                    expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0]
2023
2024                else:
2025                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"]
2026                    expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"]
2027
2028                # current instrument's price (last sellers order if buy, and last buyers order if sell):
2029                if item["direction"] == "STOP_ORDER_DIRECTION_BUY":
2030                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
2031
2032                else:
2033                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
2034
2035                # requested price when stop-order executed:
2036                target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"])
2037
2038                # price for limit-order, set up when stop-order executed:
2039                limit = NanoToFloat(item["price"]["units"], item["price"]["nano"])
2040
2041                # necessary changes in percent to reach target from current price:
2042                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
2043
2044                view["stat"]["stopOrders"].append({
2045                    "orderID": item["stopOrderId"],  # stopOrderId number parameter of current stop-order
2046                    "figi": item["figi"],  # FIGI identification
2047                    "ticker": instrument["ticker"],  # ticker name by FIGI
2048                    "lotsRequested": item["lotsRequested"],  # requested lots value
2049                    "currentPrice": lastPrice,  # current instrument's price for defined action
2050                    "targetPrice": target,  # requested price for stop-order execution in base currency
2051                    "limitPrice": limit,  # price for limit-order, set up when stop-order executed, 0 if market order
2052                    "baseCurrencyName": item["stopPrice"]["currency"],  # name of base currency
2053                    "percentChanges": changes,  # changes in percent to target from current price
2054                    "currency": item["currency"],  # instrument's currency name
2055                    "action": action,  # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS
2056                    "type": orderType,  # type of order from TKS_STOP_ORDER_TYPES
2057                    "expType": expType,  # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES
2058                    "createDate": createDate,  # string with created order date and time from UTC format (without nano seconds part)
2059                    "expDate": expDate,  # string with expiration order date and time from UTC format (without nano seconds part)
2060                })
2061
2062        # --- calculating data for analytics section:
2063        # portfolio distribution by assets:
2064        view["analytics"]["distrByAssets"] = {
2065            "Ruble": {
2066                "uniques": 1,
2067                "cost": view["stat"]["availableRUB"],
2068                "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2069            },
2070            "Currencies": {
2071                "uniques": len(view["stat"]["Currencies"]),  # all foreign currencies without RUB
2072                "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"],
2073                "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2074            },
2075            "Shares": {
2076                "uniques": len(view["stat"]["Shares"]),
2077                "cost": view["stat"]["sharesCostRUB"],
2078                "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2079            },
2080            "Bonds": {
2081                "uniques": len(view["stat"]["Bonds"]),
2082                "cost": view["stat"]["bondsCostRUB"],
2083                "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2084            },
2085            "Etfs": {
2086                "uniques": len(view["stat"]["Etfs"]),
2087                "cost": view["stat"]["etfsCostRUB"],
2088                "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2089            },
2090            "Futures": {
2091                "uniques": len(view["stat"]["Futures"]),
2092                "cost": view["stat"]["futuresCostRUB"],
2093                "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2094            },
2095        }
2096
2097        # portfolio distribution by companies:
2098        view["analytics"]["distrByCompanies"]["All money cash"] = {
2099            "ticker": "",
2100            "cost": view["stat"]["allCurrenciesCostRUB"],
2101            "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2102        }
2103        view["analytics"]["distrByCompanies"].update(byComp)
2104
2105        # portfolio distribution by sectors:
2106        view["analytics"]["distrBySectors"]["All money cash"] = {
2107            "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"],
2108            "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"],
2109        }
2110        view["analytics"]["distrBySectors"].update(bySect)
2111
2112        # portfolio distribution by currencies:
2113        if "rub" not in view["analytics"]["distrByCurrencies"].keys():
2114            view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0}
2115
2116            if self.moreDebug:
2117                uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!")
2118
2119        view["analytics"]["distrByCurrencies"].update(byCurr)
2120        view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
2121        view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
2122
2123        # portfolio distribution by countries:
2124        if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys():
2125            view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0}
2126
2127            if self.moreDebug:
2128                uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!")
2129
2130        view["analytics"]["distrByCountries"].update(byCountry)
2131        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
2132        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
2133
2134        # --- Prepare text statistics overview in human-readable:
2135        if show or onlyFiles:
2136            actualOnDate = datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)
2137
2138            # Whatever the value `details`, header not changes:
2139            info = [
2140                "# Client's portfolio\n\n",
2141                "* **Actual on date:** [{} UTC]\n".format(actualOnDate),
2142                "* **Account ID:** [{}]\n".format(self.accountId),
2143            ]
2144
2145            if details in ["full", "positions", "digest"]:
2146                info.extend([
2147                    "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2148                    "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format(
2149                        "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2150                        view["stat"]["totalChangesRUB"],
2151                        "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2152                        view["stat"]["totalChangesPercentRUB"],
2153                    ),
2154                ])
2155
2156            if details in ["full", "positions"]:
2157                info.extend([
2158                    "## Open positions\n\n",
2159                    "| Ticker [FIGI]               | Volume (blocked)                | Lots     | Curr. price  | Avg. price   | Current volume cost | Profit (%)                   |\n",
2160                    "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n",
2161                    "| **Ruble:**                  | {:>31} |          |              |              |                     |                              |\n".format(
2162                        "{:.2f} ({:.2f}) rub".format(
2163                            view["stat"]["availableRUB"],
2164                            view["stat"]["blockedRUB"],
2165                        )
2166                    )
2167                ])
2168
2169                def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list:
2170                    return [
2171                        "|                             |                                 |          |              |              |                     |                              |\n",
2172                        "| {:<27} |                                 |          |              |              | {:>19} |                              |\n".format(
2173                            noTradeStr if noTradeStr else typeStr,
2174                            "" if noTradeStr else "{:.2f} RUB".format(CostRUB),
2175                        ),
2176                    ]
2177
2178                def _InfoStr(data: dict, isCurr: bool = False) -> str:
2179                    return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format(
2180                        "{} [{}]".format(data["ticker"], data["figi"]),
2181                        "{:.2f} ({:.2f}) {}".format(
2182                            data["volume"],
2183                            data["blocked"],
2184                            data["currency"],
2185                        ) if isCurr else "{:.0f} ({:.0f})".format(
2186                            data["volume"],
2187                            data["blocked"],
2188                        ),
2189                        "—" if isCurr else "{:.4f}".format(data["lots"]).rstrip("0").rstrip("."),
2190                        "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a",
2191                        "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a",
2192                        "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]),
2193                        "{}{:.2f} {} ({}{:.2f}%)".format(
2194                            "+" if data["profit"] > 0 else "",
2195                            data["profit"], data["baseCurrencyName"],
2196                            "+" if data["percentProfit"] > 0 else "",
2197                            data["percentProfit"],
2198                        ),
2199                    )
2200
2201                # --- Show currencies section:
2202                if view["stat"]["Currencies"]:
2203                    info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**"))
2204                    for item in view["stat"]["Currencies"]:
2205                        info.append(_InfoStr(item, isCurr=True))
2206
2207                else:
2208                    info.extend(_SplitStr(noTradeStr="**Currencies:** no trades"))
2209
2210                # --- Show shares section:
2211                if view["stat"]["Shares"]:
2212                    info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**"))
2213
2214                    for item in view["stat"]["Shares"]:
2215                        info.append(_InfoStr(item))
2216
2217                else:
2218                    info.extend(_SplitStr(noTradeStr="**Shares:** no trades"))
2219
2220                # --- Show bonds section:
2221                if view["stat"]["Bonds"]:
2222                    info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**"))
2223
2224                    for item in view["stat"]["Bonds"]:
2225                        info.append(_InfoStr(item))
2226
2227                else:
2228                    info.extend(_SplitStr(noTradeStr="**Bonds:** no trades"))
2229
2230                # --- Show etfs section:
2231                if view["stat"]["Etfs"]:
2232                    info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**"))
2233
2234                    for item in view["stat"]["Etfs"]:
2235                        info.append(_InfoStr(item))
2236
2237                else:
2238                    info.extend(_SplitStr(noTradeStr="**Etfs:** no trades"))
2239
2240                # --- Show futures section:
2241                if view["stat"]["Futures"]:
2242                    info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**"))
2243
2244                    for item in view["stat"]["Futures"]:
2245                        info.append(_InfoStr(item))
2246
2247                else:
2248                    info.extend(_SplitStr(noTradeStr="**Futures:** no trades"))
2249
2250            if details in ["full", "orders"]:
2251                # --- Show pending limit orders section:
2252                if view["stat"]["orders"]:
2253                    info.extend([
2254                        "\n## Opened pending limit-orders: [{}]\n".format(len(view["stat"]["orders"])),
2255                        "\n| Ticker [FIGI]               | Order ID       | Lots (exec.) | Current price (% delta) | Target price  | Action    | Type      | Create date (UTC)       |\n",
2256                        "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n",
2257                    ])
2258
2259                    for item in view["stat"]["orders"]:
2260                        info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format(
2261                            "{} [{}]".format(item["ticker"], item["figi"]),
2262                            item["orderID"],
2263                            "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]),
2264                            "{} {} ({}{:.2f}%)".format(
2265                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2266                                item["baseCurrencyName"],
2267                                "+" if item["percentChanges"] > 0 else "",
2268                                float(item["percentChanges"]),
2269                            ),
2270                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2271                            item["action"],
2272                            item["type"],
2273                            item["date"],
2274                        ))
2275
2276                else:
2277                    info.append("\n## Total pending limit-orders: [0]\n")
2278
2279                # --- Show stop orders section:
2280                if view["stat"]["stopOrders"]:
2281                    info.extend([
2282                        "\n## Opened stop-orders: [{}]\n".format(len(view["stat"]["stopOrders"])),
2283                        "\n| Ticker [FIGI]               | Stop order ID                        | Lots   | Current price (% delta) | Target price  | Limit price   | Action    | Type        | Expire type  | Create date (UTC)   | Expiration (UTC)    |\n",
2284                        "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n",
2285                    ])
2286
2287                    for item in view["stat"]["stopOrders"]:
2288                        info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format(
2289                            "{} [{}]".format(item["ticker"], item["figi"]),
2290                            item["orderID"],
2291                            item["lotsRequested"],
2292                            "{} {} ({}{:.2f}%)".format(
2293                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2294                                item["baseCurrencyName"],
2295                                "+" if item["percentChanges"] > 0 else "",
2296                                float(item["percentChanges"]),
2297                            ),
2298                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2299                            "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"],
2300                            item["action"],
2301                            item["type"],
2302                            item["expType"],
2303                            item["createDate"],
2304                            item["expDate"],
2305                        ))
2306
2307                else:
2308                    info.append("\n## Total stop-orders: [0]\n")
2309
2310            if details in ["full", "analytics"]:
2311                # -- Show analytics section:
2312                if view["stat"]["portfolioCostRUB"] > 0:
2313                    info.extend([
2314                        "\n# Analytics\n\n"
2315                        "* **Actual on date:** [{} UTC]\n".format(actualOnDate),
2316                        "* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2317                        "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format(
2318                            "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2319                            view["stat"]["totalChangesRUB"],
2320                            "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2321                            view["stat"]["totalChangesPercentRUB"],
2322                        ),
2323                        "\n## Portfolio distribution by assets\n"
2324                        "\n| Type                               | Uniques | Percent | Current cost       |\n",
2325                        "|------------------------------------|---------|---------|--------------------|\n",
2326                    ])
2327
2328                    for key in view["analytics"]["distrByAssets"].keys():
2329                        if view["analytics"]["distrByAssets"][key]["cost"] > 0:
2330                            info.append("| {:<34} | {:<7} | {:<7} | {:<18} |\n".format(
2331                                key,
2332                                view["analytics"]["distrByAssets"][key]["uniques"],
2333                                "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]),
2334                                "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]),
2335                            ))
2336
2337                    aSepLine = "|----------------------------------------------|---------|--------------------|\n"
2338
2339                    info.extend([
2340                        "\n## Portfolio distribution by companies\n"
2341                        "\n| Company                                      | Percent | Current cost       |\n",
2342                        aSepLine,
2343                    ])
2344
2345                    for company in view["analytics"]["distrByCompanies"].keys():
2346                        if view["analytics"]["distrByCompanies"][company]["cost"] > 0:
2347                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2348                                "{}{}".format(
2349                                    "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "",
2350                                    company,
2351                                ),
2352                                "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]),
2353                                "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]),
2354                            ))
2355
2356                    info.extend([
2357                        "\n## Portfolio distribution by sectors\n"
2358                        "\n| Sector                                       | Percent | Current cost       |\n",
2359                        aSepLine,
2360                    ])
2361
2362                    for sector in view["analytics"]["distrBySectors"].keys():
2363                        if view["analytics"]["distrBySectors"][sector]["cost"] > 0:
2364                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2365                                sector,
2366                                "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]),
2367                                "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]),
2368                            ))
2369
2370                    info.extend([
2371                        "\n## Portfolio distribution by currencies\n"
2372                        "\n| Instruments currencies                       | Percent | Current cost       |\n",
2373                        aSepLine,
2374                    ])
2375
2376                    for curr in view["analytics"]["distrByCurrencies"].keys():
2377                        if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0:
2378                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2379                                "[{}] {}".format(curr, view["analytics"]["distrByCurrencies"][curr]["name"]),
2380                                "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]),
2381                                "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]),
2382                            ))
2383
2384                    info.extend([
2385                        "\n## Portfolio distribution by countries\n"
2386                        "\n| Assets by country                            | Percent | Current cost       |\n",
2387                        aSepLine,
2388                    ])
2389
2390                    for country in view["analytics"]["distrByCountries"].keys():
2391                        if view["analytics"]["distrByCountries"][country]["cost"] > 0:
2392                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2393                                country,
2394                                "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]),
2395                                "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]),
2396                            ))
2397
2398            if details in ["full", "calendar"]:
2399                # -- Show bonds payment calendar section:
2400                if view["stat"]["Bonds"]:
2401                    bondTickers = [item["ticker"] for item in view["stat"]["Bonds"]]
2402                    view["analytics"]["bondsCalendar"] = self.ExtendBondsData(instruments=bondTickers, xlsx=False)
2403                    info.append("\n" + self.ShowBondsCalendar(extBonds=view["analytics"]["bondsCalendar"], show=False))
2404
2405                else:
2406                    info.append("\n# Bond payments calendar\n\nNo bonds in the portfolio to create payments calendar\n")
2407
2408            infoText = "".join(info)
2409
2410            if show and not onlyFiles:
2411                uLogger.info(infoText)
2412
2413            if details == "full" and self.overviewFile:
2414                filename = self.overviewFile
2415
2416            elif details == "digest" and self.overviewDigestFile:
2417                filename = self.overviewDigestFile
2418
2419            elif details == "positions" and self.overviewPositionsFile:
2420                filename = self.overviewPositionsFile
2421
2422            elif details == "orders" and self.overviewOrdersFile:
2423                filename = self.overviewOrdersFile
2424
2425            elif details == "analytics" and self.overviewAnalyticsFile:
2426                filename = self.overviewAnalyticsFile
2427
2428            elif details == "calendar" and self.overviewBondsCalendarFile:
2429                filename = self.overviewBondsCalendarFile
2430
2431            else:
2432                filename = ""
2433
2434            if filename and (show or onlyFiles):
2435                with open(filename, "w", encoding="UTF-8") as fH:
2436                    fH.write(infoText)
2437
2438                uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename)))
2439
2440                if self.useHTMLReports:
2441                    htmlFilePath = filename.replace(".md", ".html") if filename.endswith(".md") else filename + ".html"
2442                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
2443                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Client's portfolio", commonCSS=COMMON_CSS, markdown=infoText))
2444
2445                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
2446
2447        return view

Get portfolio: all open positions, orders and some statistics for current accountId. If overviewFile, overviewDigestFile, overviewPositionsFile, overviewOrdersFile, overviewAnalyticsFile and overviewBondsCalendarFile are defined then also save information to file.

WARNING! It is not recommended to run this method too many times in a loop! The server receives many requests about the state of the portfolio, and then, based on the received data, a large number of calculation and statistics are collected.

Parameters
  • show: if False then only dictionary returns, if True then show more debug information.
  • details: how detailed should the information be?
    • full — shows full available information about portfolio status (by default),
    • positions — shows only open positions,
    • orders — shows only sections of open limits and stop orders.
    • digest — show a short digest of the portfolio status,
    • analytics — shows only the analytics section and the distribution of the portfolio by various categories,
    • calendar — shows only the bonds calendar section (if these present in portfolio).
  • onlyFiles: if True then do not show Markdown table in the console, but only generates report files.
Returns

dictionary with client's raw portfolio and some statistics.

def Deals( self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True, onlyFiles=False) -> tuple[list[dict], dict]:
2449    def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True, onlyFiles=False) -> tuple[list[dict], dict]:
2450        """
2451        Returns history operations between two given dates for current `accountId`.
2452        If `reportFile` string is not empty then also save human-readable report.
2453        Shows some statistical data of closed positions.
2454
2455        :param start: see docstring in `TradeRoutines.GetDatesAsString()` method.
2456        :param end: see docstring in `TradeRoutines.GetDatesAsString()` method.
2457        :param show: if `True` then also prints all records to the console.
2458        :param showCancelled: if `False` then remove information about cancelled operations from the deals report.
2459        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
2460        :return: original list of dictionaries with history of deals records from API ("operations" key):
2461                 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2462                 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc.
2463        """
2464        if self.accountId is None or not self.accountId:
2465            uLogger.error("Variable `accountId` must be defined for using this method!")
2466            raise Exception("Account ID required")
2467
2468        startDate, endDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT)  # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2469
2470        uLogger.debug("Requesting history of a client's operations. Wait, please...")
2471
2472        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2473        dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations"
2474        self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate})
2475        ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"]  # list of dict: operations returns by broker
2476        customStat = {}  # custom statistics in additional to responseJSON
2477
2478        # --- output report in human-readable format:
2479        if self.reportFile and (show or onlyFiles):
2480            splitLine1 = "|                            |                               |                              |                      |                        |\n"  # Summary section
2481            splitLine2 = "|                     |              |              |            |           |                 |            |                                                                    |\n"  # Operations section
2482            nextDay = ""
2483
2484            info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])]
2485
2486            if len(ops) > 0:
2487                customStat = {
2488                    "opsCount": 0,  # total operations count
2489                    "buyCount": 0,  # buy operations
2490                    "sellCount": 0,  # sell operations
2491                    "buyTotal": {"rub": 0.},  # Buy sums in different currencies
2492                    "sellTotal": {"rub": 0.},  # Sell sums in different currencies
2493                    "payIn": {"rub": 0.},  # Deposit brokerage account
2494                    "payOut": {"rub": 0.},  # Withdrawals
2495                    "divs": {"rub": 0.},  # Dividends income
2496                    "coupons": {"rub": 0.},  # Coupon's income
2497                    "brokerCom": {"rub": 0.},  # Service commissions
2498                    "serviceCom": {"rub": 0.},  # Service commissions
2499                    "marginCom": {"rub": 0.},  # Margin commissions
2500                    "allTaxes": {"rub": 0.},  # Sum of withholding taxes and corrections
2501                }
2502
2503                # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES:
2504                for item in ops:
2505                    if item["state"] == "OPERATION_STATE_EXECUTED":
2506                        payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2507
2508                        # count buy operations:
2509                        if "_BUY" in item["operationType"]:
2510                            customStat["buyCount"] += 1
2511
2512                            if item["payment"]["currency"] in customStat["buyTotal"].keys():
2513                                customStat["buyTotal"][item["payment"]["currency"]] += payment
2514
2515                            else:
2516                                customStat["buyTotal"][item["payment"]["currency"]] = payment
2517
2518                        # count sell operations:
2519                        elif "_SELL" in item["operationType"]:
2520                            customStat["sellCount"] += 1
2521
2522                            if item["payment"]["currency"] in customStat["sellTotal"].keys():
2523                                customStat["sellTotal"][item["payment"]["currency"]] += payment
2524
2525                            else:
2526                                customStat["sellTotal"][item["payment"]["currency"]] = payment
2527
2528                        # count incoming operations:
2529                        elif item["operationType"] in ["OPERATION_TYPE_INPUT"]:
2530                            if item["payment"]["currency"] in customStat["payIn"].keys():
2531                                customStat["payIn"][item["payment"]["currency"]] += payment
2532
2533                            else:
2534                                customStat["payIn"][item["payment"]["currency"]] = payment
2535
2536                        # count withdrawals operations:
2537                        elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]:
2538                            if item["payment"]["currency"] in customStat["payOut"].keys():
2539                                customStat["payOut"][item["payment"]["currency"]] += payment
2540
2541                            else:
2542                                customStat["payOut"][item["payment"]["currency"]] = payment
2543
2544                        # count dividends income:
2545                        elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]:
2546                            if item["payment"]["currency"] in customStat["divs"].keys():
2547                                customStat["divs"][item["payment"]["currency"]] += payment
2548
2549                            else:
2550                                customStat["divs"][item["payment"]["currency"]] = payment
2551
2552                        # count coupon's income:
2553                        elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]:
2554                            if item["payment"]["currency"] in customStat["coupons"].keys():
2555                                customStat["coupons"][item["payment"]["currency"]] += payment
2556
2557                            else:
2558                                customStat["coupons"][item["payment"]["currency"]] = payment
2559
2560                        # count broker commissions:
2561                        elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]:
2562                            if item["payment"]["currency"] in customStat["brokerCom"].keys():
2563                                customStat["brokerCom"][item["payment"]["currency"]] += payment
2564
2565                            else:
2566                                customStat["brokerCom"][item["payment"]["currency"]] = payment
2567
2568                        # count service commissions:
2569                        elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]:
2570                            if item["payment"]["currency"] in customStat["serviceCom"].keys():
2571                                customStat["serviceCom"][item["payment"]["currency"]] += payment
2572
2573                            else:
2574                                customStat["serviceCom"][item["payment"]["currency"]] = payment
2575
2576                        # count margin commissions:
2577                        elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]:
2578                            if item["payment"]["currency"] in customStat["marginCom"].keys():
2579                                customStat["marginCom"][item["payment"]["currency"]] += payment
2580
2581                            else:
2582                                customStat["marginCom"][item["payment"]["currency"]] = payment
2583
2584                        # count withholding taxes:
2585                        elif "_TAX" in item["operationType"]:
2586                            if item["payment"]["currency"] in customStat["allTaxes"].keys():
2587                                customStat["allTaxes"][item["payment"]["currency"]] += payment
2588
2589                            else:
2590                                customStat["allTaxes"][item["payment"]["currency"]] = payment
2591
2592                        else:
2593                            continue
2594
2595                customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"]
2596
2597                # --- view "Actions" lines:
2598                info.extend([
2599                    "| Report sections            |                               |                              |                      |                        |\n",
2600                    "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n",
2601                    "| **Actions:**               | Trades: {:<21} | Trading volumes:             |                      |                        |\n".format(customStat["opsCount"]),
2602                    "|                            |   Buy: {:<22} | {:<28} |                      |                        |\n".format(
2603                        "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2604                        "  rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else "  —",
2605                    ),
2606                    "|                            |   Sell: {:<21} | {:<28} |                      |                        |\n".format(
2607                        "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2608                        "  rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else "  —",
2609                    ),
2610                ])
2611
2612                opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys()))))
2613                for key in opsKeys:
2614                    if key == "rub":
2615                        continue
2616
2617                    info.extend([
2618                        "|                            |                               | {:<28} |                      |                        |\n".format(
2619                            "  {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0)
2620                        ),
2621                        "|                            |                               | {:<28} |                      |                        |\n".format(
2622                            "  {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0)
2623                        ),
2624                    ])
2625
2626                info.append(splitLine1)
2627
2628                def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str:
2629                    return "|                            | {:<29} | {:<28} | {:<20} | {:<22} |\n".format(
2630                            "  {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else "  —",
2631                            "  {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else "  —",
2632                            "  {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else "  —",
2633                            "  {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else "  —",
2634                    )
2635
2636                # --- view "Payments" lines:
2637                info.append("| **Payments:**              | Deposit on broker account:    | Withdrawals:                 | Dividends income:    | Coupons income:        |\n")
2638                paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys()))))
2639
2640                for key in paymentsKeys:
2641                    info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key))
2642
2643                info.append(splitLine1)
2644
2645                # --- view "Commissions and taxes" lines:
2646                info.append("| **Commissions and taxes:** | Broker commissions:           | Service commissions:         | Margin commissions:  | All taxes/corrections: |\n")
2647                comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys()))))
2648
2649                for key in comKeys:
2650                    info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key))
2651
2652                info.extend([
2653                    "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"),
2654                    "| Date and time       | FIGI         | Ticker       | Asset      | Value     | Payment         | Status     | Operation type                                                     |\n",
2655                    "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n",
2656                ])
2657
2658            else:
2659                info.append("Broker returned no operations during this period\n")
2660
2661            # --- view "Operations" section:
2662            for item in ops:
2663                if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]:
2664                    continue
2665
2666                else:
2667                    self._figi = item["figi"]
2668                    payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2669                    instrument = self.SearchByFIGI(requestPrice=False) if self._figi else {}
2670
2671                    # group of deals during one day:
2672                    if nextDay and item["date"].split("T")[0] != nextDay:
2673                        info.append(splitLine2)
2674                        nextDay = ""
2675
2676                    else:
2677                        nextDay = item["date"].split("T")[0]  # saving current day for splitting
2678
2679                    info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format(
2680                        item["date"].replace("T", " ").replace("Z", "").split(".")[0],
2681                        self._figi if self._figi else "—",
2682                        instrument["ticker"] if instrument else "—",
2683                        instrument["type"] if instrument else "—",
2684                        item["quantity"] if int(item["quantity"]) > 0 else "—",
2685                        "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—",
2686                        TKS_OPERATION_STATES[item["state"]],
2687                        TKS_OPERATION_TYPES[item["operationType"]],
2688                    ))
2689
2690            infoText = "".join(info)
2691
2692            if show and not onlyFiles:
2693                if self.moreDebug:
2694                    uLogger.debug("Records about history of a client's operations successfully received")
2695
2696                uLogger.info(infoText)
2697
2698            if self.reportFile and (show or onlyFiles):
2699                with open(self.reportFile, "w", encoding="UTF-8") as fH:
2700                    fH.write(infoText)
2701
2702                uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile)))
2703
2704                if self.useHTMLReports:
2705                    htmlFilePath = self.reportFile.replace(".md", ".html") if self.reportFile.endswith(".md") else self.reportFile + ".html"
2706                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
2707                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Client's operations", commonCSS=COMMON_CSS, markdown=infoText))
2708
2709                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
2710
2711        return ops, customStat

Returns history operations between two given dates for current accountId. If reportFile string is not empty then also save human-readable report. Shows some statistical data of closed positions.

Parameters
  • start: see docstring in TradeRoutines.GetDatesAsString() method.
  • end: see docstring in TradeRoutines.GetDatesAsString() method.
  • show: if True then also prints all records to the console.
  • showCancelled: if False then remove information about cancelled operations from the deals report.
  • onlyFiles: if True then do not show Markdown table in the console, but only generates report files.
Returns

original list of dictionaries with history of deals records from API ("operations" key): https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc.

def History( self, start: str = None, end: str = None, interval: str = 'hour', onlyMissing: bool = False, csvSep: str = ',', show: bool = False, onlyFiles=False) -> pandas.core.frame.DataFrame:
2713    def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False, onlyFiles=False) -> pd.DataFrame:
2714        """
2715        This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id).
2716
2717        History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`.
2718        Warning! Broker server used ISO UTC time by default.
2719
2720        If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame.
2721        Also, `historyFile` used to update history with `onlyMissing` parameter.
2722
2723        See also: `LoadHistory()` and `ShowHistoryChart()` methods.
2724
2725        :param start: see docstring in `TradeRoutines.GetDatesAsString()` method.
2726        :param end: see docstring in `TradeRoutines.GetDatesAsString()` method.
2727        :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`,
2728                         `"hour"`, `"day"`. Default: `"hour"`.
2729        :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`.
2730                            False by default. Warning! History appends only from last candle to current time
2731                            with always update last candle!
2732        :param csvSep: separator if csv-file is used, `,` by default.
2733        :param show: if `True` then also prints Pandas DataFrame to the console.
2734        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
2735        :return: Pandas DataFrame with prices history. Headers of columns are defined by default:
2736                 `["date", "time", "open", "high", "low", "close", "volume"]`.
2737        """
2738        strStartDate, strEndDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT)  # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2739        headers = ["date", "time", "open", "high", "low", "close", "volume"]  # sequence and names of column headers
2740        history = None  # empty pandas object for history
2741
2742        if interval not in TKS_CANDLE_INTERVALS.keys():
2743            uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.")
2744            raise Exception("Incorrect value")
2745
2746        if not (self._ticker or self._figi):
2747            uLogger.error("Ticker or FIGI must be defined!")
2748            raise Exception("Ticker or FIGI required")
2749
2750        if self._ticker and not self._figi:
2751            instrumentByTicker = self.SearchByTicker(requestPrice=False)
2752            self._figi = instrumentByTicker["figi"] if instrumentByTicker else ""
2753
2754        if self._figi and not self._ticker:
2755            instrumentByFIGI = self.SearchByFIGI(requestPrice=False)
2756            self._ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else ""
2757
2758        dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from start time string
2759        dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from end time string
2760        if interval.lower() != "day":
2761            dtEnd += timedelta(seconds=1)  # adds 1 sec for requests, because day end returned by `TradeRoutines.GetDatesAsString()` is 23:59:59
2762
2763        delta = dtEnd - dtStart  # current UTC time minus last time in file
2764        deltaMinutes = delta.days * 1440 + delta.seconds // 60  # minutes between start and end dates
2765
2766        # calculate history length in candles:
2767        length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1]
2768        if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0:
2769            length += 1  # to avoid fraction time
2770
2771        # calculate data blocks count:
2772        blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2]
2773
2774        uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self._ticker, self._figi))
2775        if self.moreDebug:
2776            uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end))
2777            uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate))
2778            uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval))
2779            uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2]))
2780
2781        tempOld = None  # pandas object for old history, if --only-missing key present
2782        lastTime = None  # datetime object of last old candle in file
2783
2784        if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile):
2785            if self.moreDebug:
2786                uLogger.debug("--only-missing key present, add only last missing candles...")
2787                uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile)))
2788
2789            tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers)
2790
2791            tempOld["date"] = pd.to_datetime(tempOld["date"])  # load date "as is"
2792            tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d")  # convert date to string
2793            tempOld["time"] = pd.to_datetime(tempOld["time"])  # load time "as is"
2794            tempOld["time"] = tempOld["time"].dt.strftime("%H:%M")  # convert time to string
2795
2796            # get last datetime object from last string in file or minus 1 delta if file is empty:
2797            if len(tempOld) > 0:
2798                lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2799
2800            else:
2801                lastTime = dtEnd - timedelta(days=1)  # history file is empty, so last date set at -1 day
2802
2803            tempOld = tempOld[:-1]  # always remove last old candle because it may be incompletely at the current time
2804
2805        responseJSONs = []  # raw history blocks of data
2806
2807        blockEnd = dtEnd
2808        for item in range(blocks):
2809            tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2]
2810            blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail)
2811
2812            uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format(
2813                item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2814            ))
2815
2816            if blockStart == blockEnd:
2817                uLogger.debug("Skipped this zero-length block...")
2818
2819            else:
2820                # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles
2821                historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles"
2822                self.body = str({
2823                    "figi": self._figi,
2824                    "from": blockStart.strftime(TKS_DATE_TIME_FORMAT),
2825                    "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2826                    "interval": TKS_CANDLE_INTERVALS[interval][0]
2827                })
2828                responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1)
2829
2830                if "code" in responseJSON.keys():
2831                    uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks))
2832
2833                else:
2834                    if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1:
2835                        responseJSON["candles"] = responseJSON["candles"][:-1]  # removes last candle for "yesterday" request
2836
2837                    responseJSONs = responseJSON["candles"] + responseJSONs  # add more old history behind newest dates
2838
2839            blockEnd = blockStart
2840
2841        printCount = len(responseJSONs)  # candles to show in console
2842        if responseJSONs:
2843            tempHistory = pd.DataFrame(
2844                data={
2845                    "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2846                    "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2847                    "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs],
2848                    "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs],
2849                    "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs],
2850                    "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs],
2851                    "volume": [int(item["volume"]) for item in responseJSONs],
2852                },
2853                index=range(len(responseJSONs)),
2854                columns=["date", "time", "open", "high", "low", "close", "volume"],
2855            )
2856            tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d")
2857            tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M")
2858
2859            # append only newest candles to old history if --only-missing key present:
2860            if onlyMissing and tempOld is not None and lastTime is not None:
2861                index = 0  # find start index in tempHistory data:
2862
2863                for i, item in tempHistory.iterrows():
2864                    curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2865
2866                    if curTime == lastTime:
2867                        uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
2868                        index = i
2869                        printCount = index + 1
2870                        break
2871
2872                history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True)
2873
2874            else:
2875                history = tempHistory  # if no `--only-missing` key then load full data from server
2876
2877            if self.moreDebug:
2878                uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False)))
2879
2880        if history is not None and not history.empty:
2881            if show and not onlyFiles:
2882                uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format(
2883                    strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]),
2884                    pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False),
2885                ))
2886
2887        else:
2888            uLogger.warning("Received an empty candles history!")
2889
2890        if self.historyFile is not None:
2891            if history is not None and not history.empty:
2892                history.to_csv(self.historyFile, sep=csvSep, index=False, header=False)
2893                uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self._ticker, self._figi, interval, os.path.abspath(self.historyFile)))
2894
2895            else:
2896                uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile)))
2897
2898        else:
2899            if self.moreDebug:
2900                uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.")
2901
2902        return history

This method returns last history candles of the current instrument defined by ticker or figi (FIGI id).

History returned between two given dates: start and end. Minimum requested date in the past is 1970-01-01. Warning! Broker server used ISO UTC time by default.

If historyFile is not None then method save history to file, otherwise return only Pandas DataFrame. Also, historyFile used to update history with onlyMissing parameter.

See also: LoadHistory() and ShowHistoryChart() methods.

Parameters
  • start: see docstring in TradeRoutines.GetDatesAsString() method.
  • end: see docstring in TradeRoutines.GetDatesAsString() method.
  • interval: this is a candle interval. Current available values are "1min", "5min", "15min", "hour", "day". Default: "hour".
  • onlyMissing: if True then add only last missing candles, do not request all history length from start. False by default. Warning! History appends only from last candle to current time with always update last candle!
  • csvSep: separator if csv-file is used, , by default.
  • show: if True then also prints Pandas DataFrame to the console.
  • onlyFiles: if True then do not show Markdown table in the console, but only generates report files.
Returns

Pandas DataFrame with prices history. Headers of columns are defined by default: ["date", "time", "open", "high", "low", "close", "volume"].

def LoadHistory(self, filePath: str) -> pandas.core.frame.DataFrame:
2904    def LoadHistory(self, filePath: str) -> pd.DataFrame:
2905        """
2906        Load candles history from csv-file and return Pandas DataFrame object.
2907
2908        See also: `History()` and `ShowHistoryChart()` methods.
2909
2910        :param filePath: path to csv-file to open.
2911        """
2912        loadedHistory = None  # init candles data object
2913
2914        uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...")
2915
2916        if os.path.exists(filePath):
2917            loadedHistory = self.priceModel.LoadFromFile(filePath)  # load data and get chain of candles as Pandas DataFrame
2918
2919            tfStr = self.priceModel.FormattedDelta(
2920                self.priceModel.timeframe,
2921                "{days} days {hours}h {minutes}m {seconds}s",
2922            ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta(
2923                self.priceModel.timeframe,
2924                "{hours}h {minutes}m {seconds}s",
2925            )
2926
2927            if loadedHistory is not None and not loadedHistory.empty:
2928                uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format(
2929                    len(loadedHistory),
2930                    tfStr,
2931                    pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)),
2932                )
2933
2934            else:
2935                uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath)))
2936
2937        else:
2938            uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath))
2939
2940        return loadedHistory

Load candles history from csv-file and return Pandas DataFrame object.

See also: History() and ShowHistoryChart() methods.

Parameters
  • filePath: path to csv-file to open.
def ShowHistoryChart( self, candles: Union[str, pandas.core.frame.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None:
2942    def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None:
2943        """
2944        Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file.
2945
2946        Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart.
2947        Default: `index.html` (both for interact and non-interact candlesticks chart).
2948
2949        See also: `History()` and `LoadHistory()` methods.
2950
2951        :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object.
2952        :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart.
2953                         See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters
2954                         If False then chain of candlesticks will render as not interactive Google Candlestick chart.
2955                         See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template
2956        :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to
2957                              html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file.
2958        """
2959        if isinstance(candles, str):
2960            self.priceModel.prices = self.LoadHistory(filePath=candles)  # load candles chain from file
2961            self.priceModel.ticker = os.path.basename(candles)  # use filename as ticker name in PriceGenerator
2962
2963        elif isinstance(candles, pd.DataFrame):
2964            self.priceModel.prices = candles  # set candles chain from variable
2965            self.priceModel.ticker = self._ticker  # use current TKSBrokerAPI ticker as ticker name in PriceGenerator
2966
2967            if "datetime" not in candles.columns:
2968                self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True)  # PriceGenerator uses "datetime" column with date and time
2969
2970        else:
2971            uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!")
2972            raise Exception("Incorrect value")
2973
2974        self.priceModel.horizon = len(self.priceModel.prices)  # use length of candles data as horizon in PriceGenerator
2975
2976        if interact:
2977            uLogger.debug("Rendering interactive candles chart. Wait, please...")
2978
2979            self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2980
2981        else:
2982            uLogger.debug("Rendering non-interactive candles chart. Wait, please...")
2983
2984            self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2985
2986        uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile)))

Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file.

Self variable htmlHistoryFile can be use as html-file name to save interaction or non-interaction chart. Default: index.html (both for interact and non-interact candlesticks chart).

See also: History() and LoadHistory() methods.

Parameters
def Trade( self, operation: str, lots: int = 1, tp: float = 0.0, sl: float = 0.0, expDate: str = 'Undefined') -> dict:
2988    def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2989        """
2990        Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response.
2991        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
2992
2993        See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`.
2994
2995        :param operation: string "Buy" or "Sell".
2996        :param lots: volume, integer count of lots >= 1.
2997        :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`.
2998        :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`.
2999        :param expDate: string "Undefined" by default or local date in future,
3000                        it is a string with format `%Y-%m-%d %H:%M:%S`.
3001        :return: JSON with response from broker server.
3002        """
3003        if self.accountId is None or not self.accountId:
3004            uLogger.error("Variable `accountId` must be defined for using this method!")
3005            raise Exception("Account ID required")
3006
3007        if operation is None or not operation or operation not in ("Buy", "Sell"):
3008            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
3009            raise Exception("Incorrect value")
3010
3011        if lots is None or lots < 1:
3012            uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.")
3013            lots = 1
3014
3015        if tp is None or tp < 0:
3016            tp = 0
3017
3018        if sl is None or sl < 0:
3019            sl = 0
3020
3021        if expDate is None or not expDate:
3022            expDate = "Undefined"
3023
3024        if not (self._ticker or self._figi):
3025            uLogger.error("Ticker or FIGI must be defined!")
3026            raise Exception("Ticker or FIGI required")
3027
3028        instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True)
3029        self._ticker = instrument["ticker"]
3030        self._figi = instrument["figi"]
3031
3032        uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self._ticker, self._figi, lots, tp, sl, expDate))
3033
3034        openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
3035        self.body = str({
3036            "figi": self._figi,
3037            "quantity": str(lots),
3038            "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
3039            "accountId": str(self.accountId),
3040            "orderType": "ORDER_TYPE_MARKET",  # see: TKS_ORDER_TYPES
3041        })
3042        response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0)
3043
3044        if "orderId" in response.keys():
3045            uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format(
3046                operation, response["orderId"],
3047                self._ticker, self._figi, lots,
3048                NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"],
3049                NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"],
3050                NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"],
3051            ))
3052
3053            if tp > 0:
3054                self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate)
3055
3056            if sl > 0:
3057                self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate)
3058
3059        else:
3060            uLogger.warning("Not `oK` status received! Market order not executed. See full debug log and try again open order later.")
3061
3062        return response

Universal method to create market order and make deal at the current price for current accountId. Returns JSON data with response. If tp or sl > 0, then in additional will open stop-orders with "TP" and "SL" flags for stopType parameter.

See also: Order() docstring. More simple methods than Trade() are Buy() and Sell().

Parameters
  • operation: string "Buy" or "Sell".
  • lots: volume, integer count of lots >= 1.
  • tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter targetPrice in self.Order().
  • sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter targetPrice in self.Order().
  • expDate: string "Undefined" by default or local date in future, it is a string with format %Y-%m-%d %H:%M:%S.
Returns

JSON with response from broker server.

def Buy( self, lots: int = 1, tp: float = 0.0, sl: float = 0.0, expDate: str = 'Undefined') -> dict:
3064    def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
3065        """
3066        More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response.
3067        If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter.
3068
3069        See also: `Order()` and `Trade()` docstrings.
3070
3071        :param lots: volume, integer count of lots >= 1.
3072        :param tp: float > 0, take profit price of stop-order.
3073        :param sl: float > 0, stop loss price of stop-order.
3074        :param expDate: it's a local date in future.
3075                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3076        :return: JSON with response from broker server.
3077        """
3078        return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate)

More simple method than Trade(). Create Buy market order and make deal at the current price. Returns JSON data with response. If tp or sl > 0, then in additional will opens stop-orders with "TP" and "SL" flags for stopType parameter.

See also: Order() and Trade() docstrings.

Parameters
  • lots: volume, integer count of lots >= 1.
  • tp: float > 0, take profit price of stop-order.
  • sl: float > 0, stop loss price of stop-order.
  • expDate: it's a local date in future. String has a format like this: %Y-%m-%d %H:%M:%S.
Returns

JSON with response from broker server.

def Sell( self, lots: int = 1, tp: float = 0.0, sl: float = 0.0, expDate: str = 'Undefined') -> dict:
3080    def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
3081        """
3082        More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response.
3083        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
3084
3085        See also: `Order()` and `Trade()` docstrings.
3086
3087        :param lots: volume, integer count of lots >= 1.
3088        :param tp: float > 0, take profit price of stop-order.
3089        :param sl: float > 0, stop loss price of stop-order.
3090        :param expDate: it's a local date in the future.
3091                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3092        :return: JSON with response from broker server.
3093        """
3094        return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate)

More simple method than Trade(). Create Sell market order and make deal at the current price. Returns JSON data with response. If tp or sl > 0, then in additional will open stop-orders with "TP" and "SL" flags for stopType parameter.

See also: Order() and Trade() docstrings.

Parameters
  • lots: volume, integer count of lots >= 1.
  • tp: float > 0, take profit price of stop-order.
  • sl: float > 0, stop loss price of stop-order.
  • expDate: it's a local date in the future. String has a format like this: %Y-%m-%d %H:%M:%S.
Returns

JSON with response from broker server.

def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None:
3096    def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None:
3097        """
3098        Close position of given instruments.
3099
3100        :param instruments: list of instruments defined by tickers or FIGIs that must be closed.
3101        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
3102                         This avoids unnecessary downloading data from the server.
3103        """
3104        if instruments is None or not instruments:
3105            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
3106            raise Exception("Ticker or FIGI required")
3107
3108        if isinstance(instruments, str):
3109            instruments = [instruments]
3110
3111        uniqueInstruments = self.GetUniqueFIGIs(instruments)
3112        if uniqueInstruments:
3113            if portfolio is None or not portfolio:
3114                portfolio = self.Overview(show=False)
3115
3116            allOpened = [item["figi"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]]
3117            uLogger.debug("All opened instruments by it's FIGI: {}".format(", ".join(allOpened)))
3118
3119            for self._figi in uniqueInstruments:
3120                if self._figi not in allOpened:
3121                    uLogger.warning("Instrument with FIGI [{}] not in open positions list!".format(self._figi))
3122                    continue
3123
3124                # search open trade info about instrument by ticker:
3125                instrument = {}
3126                for iType in TKS_INSTRUMENTS:
3127                    if instrument:
3128                        break
3129
3130                    for item in portfolio["stat"][iType]:
3131                        if item["figi"] == self._figi:
3132                            instrument = item
3133                            break
3134
3135                if instrument:
3136                    self._ticker = instrument["ticker"]
3137                    self._figi = instrument["figi"]
3138
3139                    uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format(
3140                        self._ticker,
3141                        self._figi,
3142                        int(instrument["volume"]),
3143                        ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "",
3144                    ))
3145
3146                    tradeLots = abs(instrument["lots"]) - instrument["blocked"]  # available volumes in lots for close operation
3147
3148                    if tradeLots > 0:
3149                        if instrument["blocked"] > 0:
3150                            uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format(
3151                                instrument["blocked"],
3152                                self._ticker,
3153                                tradeLots,
3154                            ))
3155
3156                        # if direction is "Long" then we need sell, if direction is "Short" then we need buy:
3157                        self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots)
3158
3159                    else:
3160                        uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self._ticker))

Close position of given instruments.

Parameters
  • instruments: list of instruments defined by tickers or FIGIs that must be closed.
  • portfolio: pre-received dictionary with open trades, returned by Overview() method. This avoids unnecessary downloading data from the server.
def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None:
3162    def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None:
3163        """
3164        Close all positions of given instruments with defined type.
3165
3166        :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list.
3167        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
3168                         This avoids unnecessary downloading data from the server.
3169        """
3170        if iType not in TKS_INSTRUMENTS:
3171            uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType))
3172
3173        else:
3174            if portfolio is None or not portfolio:
3175                portfolio = self.Overview(show=False)
3176
3177            tickers = [item["ticker"] for item in portfolio["stat"][iType]]
3178            uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers))
3179
3180            if tickers and portfolio:
3181                self.CloseTrades(tickers, portfolio)
3182
3183            else:
3184                uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType))

Close all positions of given instruments with defined type.

Parameters
  • iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list.
  • portfolio: pre-received dictionary with open trades, returned by Overview() method. This avoids unnecessary downloading data from the server.
def Order( self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0.0, stopType: str = 'Limit', expDate: str = 'Undefined') -> dict:
3186    def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3187        """
3188        Universal method to create market or limit orders with all available parameters for current `accountId`.
3189        See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`.
3190
3191        If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above
3192        current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day.
3193
3194        Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell"
3195        then broker immediately open market order as you can do simple --buy or --sell operations!
3196
3197        If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell".
3198        When current price will go up or down to target price value then broker opens a limit order.
3199        Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter.
3200
3201        Only one attempt and no retry for opens order. If network issue occurred you can create new request.
3202
3203        :param operation: string "Buy" or "Sell".
3204        :param orderType: string "Limit" or "Stop".
3205        :param lots: volume, integer count of lots >= 1.
3206        :param targetPrice: target price > 0. This is open trade price for limit order.
3207        :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice.
3208                           Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order.
3209        :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types
3210                         "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3211                         Stop loss order always executed by market price.
3212        :param expDate: string "Undefined" by default or local date in future.
3213                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3214                        This date is converting to UTC format for server. This parameter only makes sense for stop-order.
3215                        A limit order has no expiration date, it lasts until the end of the trading day.
3216        :return: JSON with response from broker server.
3217        """
3218        if self.accountId is None or not self.accountId:
3219            uLogger.error("Variable `accountId` must be defined for using this method!")
3220            raise Exception("Account ID required")
3221
3222        if operation is None or not operation or operation not in ("Buy", "Sell"):
3223            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
3224            raise Exception("Incorrect value")
3225
3226        if orderType is None or not orderType or orderType not in ("Limit", "Stop"):
3227            uLogger.error("You must define order type only one of them: `Limit` or `Stop`!")
3228            raise Exception("Incorrect value")
3229
3230        if lots is None or lots < 1:
3231            uLogger.error("You must define trade volume > 0: integer count of lots!")
3232            raise Exception("Incorrect value")
3233
3234        if targetPrice is None or targetPrice <= 0:
3235            uLogger.error("Target price for limit-order must be greater than 0!")
3236            raise Exception("Incorrect value")
3237
3238        if limitPrice is None or limitPrice <= 0:
3239            limitPrice = targetPrice
3240
3241        if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"):
3242            stopType = "Limit"
3243
3244        if expDate is None or not expDate:
3245            expDate = "Undefined"
3246
3247        if not (self._ticker or self._figi):
3248            uLogger.error("Tocker or FIGI must be defined!")
3249            raise Exception("Ticker or FIGI required")
3250
3251        response = {}
3252        instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True)
3253        self._ticker = instrument["ticker"]
3254        self._figi = instrument["figi"]
3255
3256        if orderType == "Limit":
3257            uLogger.debug(
3258                "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format(
3259                    self._ticker, self._figi,
3260                    operation, lots, targetPrice, instrument["currency"],
3261                ))
3262
3263            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
3264            self.body = str({
3265                "figi": self._figi,
3266                "quantity": str(lots),
3267                "price": FloatToNano(targetPrice),
3268                "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
3269                "accountId": str(self.accountId),
3270                "orderType": "ORDER_TYPE_LIMIT",  # see: TKS_ORDER_TYPES
3271            })
3272            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0)
3273
3274            if "orderId" in response.keys():
3275                uLogger.info(
3276                    "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{} {}]".format(
3277                        response["orderId"], self._ticker, self._figi, operation, lots,
3278                        "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"],
3279                    ))
3280
3281                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3282                    if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]:
3283                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format(
3284                            targetPrice, instrument["currency"],
3285                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3286                        ))
3287
3288                    if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]:
3289                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format(
3290                            targetPrice, instrument["currency"],
3291                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3292                        ))
3293
3294            else:
3295                uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log and try again open order later.")
3296
3297        if orderType == "Stop":
3298            uLogger.debug(
3299                "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format(
3300                    self._ticker, self._figi,
3301                    operation, lots,
3302                    targetPrice, instrument["currency"],
3303                    limitPrice, instrument["currency"],
3304                    stopType, expDate,
3305                ))
3306
3307            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder"
3308            expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT)
3309            stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT"
3310
3311            body = {
3312                "figi": self._figi,
3313                "quantity": str(lots),
3314                "price": FloatToNano(limitPrice),
3315                "stopPrice": FloatToNano(targetPrice),
3316                "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL",  # see: TKS_STOP_ORDER_DIRECTIONS
3317                "accountId": str(self.accountId),
3318                "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL",  # see: TKS_STOP_ORDER_EXPIRATION_TYPES
3319                "stopOrderType": stopOrderType,  # see: TKS_STOP_ORDER_TYPES
3320            }
3321
3322            if expDateUTC:
3323                body["expireDate"] = expDateUTC
3324
3325            self.body = str(body)
3326            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0)
3327
3328            if "stopOrderId" in response.keys():
3329                uLogger.info(
3330                    "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{} {}], limit price [{} {}], stop-order type [{}] and expiration date [{} UTC]".format(
3331                        response["stopOrderId"], self._ticker, self._figi, operation, lots,
3332                        "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"],
3333                        "{:.4f}".format(limitPrice).rstrip("0").rstrip("."), instrument["currency"],
3334                        TKS_STOP_ORDER_TYPES[stopOrderType],
3335                        datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"],
3336                    ))
3337
3338                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3339                    if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3340                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3341                            targetPrice, instrument["currency"],
3342                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3343                        ))
3344
3345                    if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3346                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3347                            targetPrice, instrument["currency"],
3348                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3349                        ))
3350
3351            else:
3352                uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log and try again open order later.")
3353
3354        return response

Universal method to create market or limit orders with all available parameters for current accountId. See more simple methods: BuyLimit(), BuyStop(), SellLimit(), SellStop().

If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day.

Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell" then broker immediately open market order as you can do simple --buy or --sell operations!

If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell". When current price will go up or down to target price value then broker opens a limit order. Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter.

Only one attempt and no retry for opens order. If network issue occurred you can create new request.

Parameters
  • operation: string "Buy" or "Sell".
  • orderType: string "Limit" or "Stop".
  • lots: volume, integer count of lots >= 1.
  • targetPrice: target price > 0. This is open trade price for limit order.
  • limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice. Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order.
  • stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly. Stop loss order always executed by market price.
  • expDate: string "Undefined" by default or local date in future. String has a format like this: %Y-%m-%d %H:%M:%S. This date is converting to UTC format for server. This parameter only makes sense for stop-order. A limit order has no expiration date, it lasts until the end of the trading day.
Returns

JSON with response from broker server.

def BuyLimit(self, lots: int, targetPrice: float) -> dict:
3356    def BuyLimit(self, lots: int, targetPrice: float) -> dict:
3357        """
3358        Create pending `Buy` limit-order (below current price). You must specify only 2 parameters:
3359        `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then
3360        broker immediately open `Buy` market order, such as if you do simple `--buy` operation!
3361        See also: `Order()` docstring.
3362
3363        :param lots: volume, integer count of lots >= 1.
3364        :param targetPrice: target price > 0. This is open trade price for limit order.
3365        :return: JSON with response from broker server.
3366        """
3367        return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice)

Create pending Buy limit-order (below current price). You must specify only 2 parameters: lots and target price to open buy limit-order. If you try to create buy limit-order above current price then broker immediately open Buy market order, such as if you do simple --buy operation! See also: Order() docstring.

Parameters
  • lots: volume, integer count of lots >= 1.
  • targetPrice: target price > 0. This is open trade price for limit order.
Returns

JSON with response from broker server.

def BuyStop( self, lots: int, targetPrice: float, limitPrice: float = 0.0, stopType: str = 'Limit', expDate: str = 'Undefined') -> dict:
3369    def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3370        """
3371        Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order.
3372        In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3373        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3374        target price value then broker opens a limit order. See also: `Order()` docstring.
3375
3376        :param lots: volume, integer count of lots >= 1.
3377        :param targetPrice: target price > 0. This is trigger price for buy stop-order.
3378        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3379                           with price equal to limitPrice, when current price goes to target price of buy stop-order.
3380        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3381                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3382        :param expDate: string "Undefined" by default or local date in future.
3383                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3384                        This date is converting to UTC format for server.
3385        :return: JSON with response from broker server.
3386        """
3387        return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)

Create Buy stop-order. You must specify at least 2 parameters: lots target price to open buy stop-order. In additional you can specify 3 parameters for buy stop-order: limit price >=0, stop type = Limit|SL|TP, expiration date = Undefined|%%Y-%%m-%%d %%H:%%M:%%S. When current price will go up or down to target price value then broker opens a limit order. See also: Order() docstring.

Parameters
  • lots: volume, integer count of lots >= 1.
  • targetPrice: target price > 0. This is trigger price for buy stop-order.
  • limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of buy stop-order.
  • stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
  • expDate: string "Undefined" by default or local date in future. String has a format like this: %Y-%m-%d %H:%M:%S. This date is converting to UTC format for server.
Returns

JSON with response from broker server.

def SellLimit(self, lots: int, targetPrice: float) -> dict:
3389    def SellLimit(self, lots: int, targetPrice: float) -> dict:
3390        """
3391        Create pending `Sell` limit-order (above current price). You must specify only 2 parameters:
3392        `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then
3393        broker immediately open `Sell` market order, such as if you do simple `--sell` operation!
3394        See also: `Order()` docstring.
3395
3396        :param lots: volume, integer count of lots >= 1.
3397        :param targetPrice: target price > 0. This is open trade price for limit order.
3398        :return: JSON with response from broker server.
3399        """
3400        return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice)

Create pending Sell limit-order (above current price). You must specify only 2 parameters: lots and target price to open sell limit-order. If you try to create sell limit-order below current price then broker immediately open Sell market order, such as if you do simple --sell operation! See also: Order() docstring.

Parameters
  • lots: volume, integer count of lots >= 1.
  • targetPrice: target price > 0. This is open trade price for limit order.
Returns

JSON with response from broker server.

def SellStop( self, lots: int, targetPrice: float, limitPrice: float = 0.0, stopType: str = 'Limit', expDate: str = 'Undefined') -> dict:
3402    def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3403        """
3404        Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order.
3405        In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3406        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3407        target price value then broker opens a limit order. See also: `Order()` docstring.
3408
3409        :param lots: volume, integer count of lots >= 1.
3410        :param targetPrice: target price > 0. This is trigger price for sell stop-order.
3411        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3412                           with price equal to limitPrice, when current price goes to target price of sell stop-order.
3413        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3414                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3415        :param expDate: string "Undefined" by default or local date in future.
3416                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3417                        This date is converting to UTC format for server.
3418        :return: JSON with response from broker server.
3419        """
3420        return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)

Create Sell stop-order. You must specify at least 2 parameters: lots target price to open sell stop-order. In additional you can specify 3 parameters for sell stop-order: limit price >=0, stop type = Limit|SL|TP, expiration date = Undefined|%%Y-%%m-%%d %%H:%%M:%%S. When current price will go up or down to target price value then broker opens a limit order. See also: Order() docstring.

Parameters
  • lots: volume, integer count of lots >= 1.
  • targetPrice: target price > 0. This is trigger price for sell stop-order.
  • limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of sell stop-order.
  • stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
  • expDate: string "Undefined" by default or local date in future. String has a format like this: %Y-%m-%d %H:%M:%S. This date is converting to UTC format for server.
Returns

JSON with response from broker server.

def CloseOrders( self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None:
3422    def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None:
3423        """
3424        Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`.
3425
3426        :param orderIDs: list of integers with `orderId` or `stopOrderId`.
3427        :param allOrdersIDs: pre-received lists of all active pending limit orders.
3428                             This avoids unnecessary downloading data from the server.
3429        :param allStopOrdersIDs: pre-received lists of all active stop orders.
3430        """
3431        if self.accountId is None or not self.accountId:
3432            uLogger.error("Variable `accountId` must be defined for using this method!")
3433            raise Exception("Account ID required")
3434
3435        if orderIDs:
3436            if allOrdersIDs is None:
3437                rawOrders = self.RequestPendingOrders()
3438                allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending limit orders ID
3439
3440            if allStopOrdersIDs is None:
3441                rawStopOrders = self.RequestStopOrders()
3442                allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3443
3444            for orderID in orderIDs:
3445                idInPendingOrders = orderID in allOrdersIDs
3446                idInStopOrders = orderID in allStopOrdersIDs
3447
3448                if not (idInPendingOrders or idInStopOrders):
3449                    uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID))
3450                    continue
3451
3452                else:
3453                    if idInPendingOrders:
3454                        uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID))
3455
3456                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder
3457                        self.body = str({"accountId": self.accountId, "orderId": orderID})
3458                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder"
3459                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3460
3461                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3462                            if self.moreDebug:
3463                                uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3464
3465                            uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID))
3466
3467                        else:
3468                            uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID))
3469
3470                    elif idInStopOrders:
3471                        uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID))
3472
3473                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder
3474                        self.body = str({"accountId": self.accountId, "stopOrderId": orderID})
3475                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder"
3476                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3477
3478                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3479                            if self.moreDebug:
3480                                uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3481
3482                            uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID))
3483
3484                        else:
3485                            uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID))
3486
3487                    else:
3488                        continue

Cancel order or list of orders by its orderId or stopOrderId for current accountId.

Parameters
  • orderIDs: list of integers with orderId or stopOrderId.
  • allOrdersIDs: pre-received lists of all active pending limit orders. This avoids unnecessary downloading data from the server.
  • allStopOrdersIDs: pre-received lists of all active stop orders.
def CloseAllOrders(self) -> None:
3490    def CloseAllOrders(self) -> None:
3491        """
3492        Gets a list of open pending and stop orders and cancel it all.
3493        """
3494        rawOrders = self.RequestPendingOrders()
3495        allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending limit orders ID
3496        lenOrders = len(allOrdersIDs)
3497
3498        rawStopOrders = self.RequestStopOrders()
3499        allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3500        lenSOrders = len(allStopOrdersIDs)
3501
3502        if lenOrders > 0 or lenSOrders > 0:
3503            uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders))
3504
3505            self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs)
3506
3507        else:
3508            uLogger.info("Orders not found, nothing to cancel.")

Gets a list of open pending and stop orders and cancel it all.

def CloseAll(self, *args) -> None:
3510    def CloseAll(self, *args) -> None:
3511        """
3512        Close all available (not blocked) opened trades and orders.
3513
3514        Also, you can select one or more keywords case-insensitive:
3515        `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type.
3516
3517        Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods.
3518        """
3519        overview = self.Overview(show=False)  # get all open trades info
3520
3521        if len(args) == 0:
3522            uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...")
3523            self.CloseAllOrders()  # close all pending and stop orders
3524
3525            for iType in TKS_INSTRUMENTS:
3526                if iType != "Currencies":
3527                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies
3528
3529        else:
3530            uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args)))
3531            lowerArgs = [x.lower() for x in args]
3532
3533            if "orders" in lowerArgs:
3534                self.CloseAllOrders()  # close all pending and stop orders
3535
3536            for iType in TKS_INSTRUMENTS:
3537                if iType.lower() in lowerArgs and iType != "Currencies":
3538                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies

Close all available (not blocked) opened trades and orders.

Also, you can select one or more keywords case-insensitive: orders, shares, bonds, etfs and futures from TKS_INSTRUMENTS enum to specify trades type.

Currency positions you must close manually using buy or sell operations, CloseTrades() or CloseAllTrades() methods.

def CloseAllByTicker(self, instrument: str) -> None:
3540    def CloseAllByTicker(self, instrument: str) -> None:
3541        """
3542        Close all available (not blocked) opened trades and orders for one instrument defined by its ticker.
3543
3544        This method searches opened trade and orders of instrument throw all portfolio and then use
3545        `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument.
3546
3547        See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`.
3548
3549        :param instrument: string with ticker.
3550        """
3551        if instrument is None or not instrument:
3552            uLogger.error("Ticker name must be defined for using this method!")
3553            raise Exception("Ticker required")
3554
3555        overview = self.Overview(show=False)  # get user portfolio with all open trades info
3556
3557        self._ticker = instrument  # try to set instrument as ticker
3558        self._figi = ""
3559
3560        limitAll = [item["orderID"] for item in overview["stat"]["orders"]]  # list of all pending limit order IDs
3561        stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]]  # list of all stop order IDs
3562
3563        if limitAll and self.IsInLimitOrders(portfolio=overview):
3564            uLogger.debug("Closing all opened pending limit orders for the instrument with ticker [{}]. Wait, please...")
3565            self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3566
3567        if stopAll and self.IsInStopOrders(portfolio=overview):
3568            uLogger.debug("Closing all opened stop orders for the instrument with ticker [{}]. Wait, please...")
3569            self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3570
3571        if self.IsInPortfolio(portfolio=overview):
3572            uLogger.debug("Closing all available (not blocked) opened trade for the instrument with ticker [{}]. Wait, please...")
3573            self.CloseTrades(instruments=[instrument], portfolio=overview)

Close all available (not blocked) opened trades and orders for one instrument defined by its ticker.

This method searches opened trade and orders of instrument throw all portfolio and then use CloseTrades() and CloseOrders() methods to close trade and cancel all orders for that instrument.

See also: IsInLimitOrders(), GetLimitOrderIDs(), IsInStopOrders(), GetStopOrderIDs(), CloseTrades() and CloseOrders().

Parameters
  • instrument: string with ticker.
def CloseAllByFIGI(self, instrument: str) -> None:
3575    def CloseAllByFIGI(self, instrument: str) -> None:
3576        """
3577        Close all available (not blocked) opened trades and orders for one instrument defined by its FIGI id.
3578
3579        This method searches opened trade and orders of instrument throw all portfolio and then use
3580        `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument.
3581
3582        See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`.
3583
3584        :param instrument: string with FIGI id.
3585        """
3586        if instrument is None or not instrument:
3587            uLogger.error("FIGI id must be defined for using this method!")
3588            raise Exception("FIGI required")
3589
3590        overview = self.Overview(show=False)  # get user portfolio with all open trades info
3591
3592        self._ticker = ""
3593        self._figi = instrument  # try to set instrument as FIGI id
3594
3595        limitAll = [item["orderID"] for item in overview["stat"]["orders"]]  # list of all pending limit order IDs
3596        stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]]  # list of all stop order IDs
3597
3598        if limitAll and self.IsInLimitOrders(portfolio=overview):
3599            uLogger.debug("Closing all opened pending limit orders for the instrument with FIGI [{}]. Wait, please...")
3600            self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3601
3602        if stopAll and self.IsInStopOrders(portfolio=overview):
3603            uLogger.debug("Closing all opened stop orders for the instrument with FIGI [{}]. Wait, please...")
3604            self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3605
3606        if self.IsInPortfolio(portfolio=overview):
3607            uLogger.debug("Closing all available (not blocked) opened trade for the instrument with FIGI [{}]. Wait, please...")
3608            self.CloseTrades(instruments=[instrument], portfolio=overview)

Close all available (not blocked) opened trades and orders for one instrument defined by its FIGI id.

This method searches opened trade and orders of instrument throw all portfolio and then use CloseTrades() and CloseOrders() methods to close trade and cancel all orders for that instrument.

See also: IsInLimitOrders(), GetLimitOrderIDs(), IsInStopOrders(), GetStopOrderIDs(), CloseTrades() and CloseOrders().

Parameters
  • instrument: string with FIGI id.
@staticmethod
def ParseOrderParameters(operation, **inputParameters):
3610    @staticmethod
3611    def ParseOrderParameters(operation, **inputParameters):
3612        """
3613        Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders.
3614
3615        :param operation: string "Buy" or "Sell".
3616        :param inputParameters: this is dict of strings that looks like this
3617               `{"lots": "L_int,...", "prices": "P_float,..."}` where
3618               "lots" key: one or more lot values (integer numbers) to open with every limit-order
3619               "prices" key: one or more prices to open limit-orders
3620               Counts of values in lots and prices lists must be equals!
3621        :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]`
3622        """
3623        # TODO: update order grid work with api v2
3624        pass
3625        # uLogger.debug("Input parameters: {}".format(inputParameters))
3626        #
3627        # if operation is None or not operation or operation not in ("Buy", "Sell"):
3628        #     uLogger.error("You must define operation type: 'Buy' or 'Sell'!")
3629        #     raise Exception("Incorrect value")
3630        #
3631        # if "l" in inputParameters.keys():
3632        #     inputParameters["lots"] = inputParameters.pop("l")
3633        #
3634        # if "p" in inputParameters.keys():
3635        #     inputParameters["prices"] = inputParameters.pop("p")
3636        #
3637        # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys():
3638        #     uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!")
3639        #     raise Exception("Incorrect value")
3640        #
3641        # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")]
3642        # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")]
3643        #
3644        # if len(lots) != len(prices):
3645        #     uLogger.error("'lots' and 'prices' lists must have equal length of values!")
3646        #     raise Exception("Incorrect value")
3647        #
3648        # uLogger.debug("Extracted parameters for orders:")
3649        # uLogger.debug("lots = {}".format(lots))
3650        # uLogger.debug("prices = {}".format(prices))
3651        #
3652        # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...]
3653        # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))]
3654        # uLogger.debug("Order parameters: {}".format(result))
3655        #
3656        # return result

Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders.

Parameters
  • operation: string "Buy" or "Sell".
  • inputParameters: this is dict of strings that looks like this {"lots": "L_int,...", "prices": "P_float,..."} where "lots" key: one or more lot values (integer numbers) to open with every limit-order "prices" key: one or more prices to open limit-orders Counts of values in lots and prices lists must be equals!
Returns

list of dictionaries with all lots and prices to open orders that looks like this [{"lot": lots_1, "price": price_1}, {...}, ...]

def IsInPortfolio(self, portfolio: dict = None) -> bool:
3658    def IsInPortfolio(self, portfolio: dict = None) -> bool:
3659        """
3660        Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`.
3661
3662        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3663        :return: `True` if portfolio contains open position with given instrument, `False` otherwise.
3664        """
3665        result = False
3666        msg = "Instrument not defined!"
3667
3668        if portfolio is None or not portfolio:
3669            portfolio = self.Overview(show=False)
3670
3671        if self._ticker:
3672            uLogger.debug("Searching instrument with ticker [{}] throw opened positions list...".format(self._ticker))
3673            msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker)
3674
3675            for iType in TKS_INSTRUMENTS:
3676                for instrument in portfolio["stat"][iType]:
3677                    if instrument["ticker"] == self._ticker:
3678                        result = True
3679                        msg = "Instrument with ticker [{}] is present in open positions".format(self._ticker)
3680                        break
3681
3682        elif self._figi:
3683            uLogger.debug("Searching instrument with FIGI [{}] throw opened positions list...".format(self._figi))
3684            msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi)
3685
3686            for iType in TKS_INSTRUMENTS:
3687                for instrument in portfolio["stat"][iType]:
3688                    if instrument["figi"] == self._figi:
3689                        result = True
3690                        msg = "Instrument with FIGI [{}] is present in open positions".format(self._figi)
3691                        break
3692
3693        else:
3694            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3695
3696        uLogger.debug(msg)
3697
3698        return result

Checks if instrument is in the user's portfolio. Instrument must be defined by ticker (highly priority) or figi.

Parameters
  • portfolio: dict with user's portfolio data. If None, then requests portfolio from Overview() method.
Returns

True if portfolio contains open position with given instrument, False otherwise.

def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict:
3700    def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict:
3701        """
3702        Returns instrument from the user's portfolio if it presents there.
3703        Instrument must be defined by `ticker` (highly priority) or `figi`.
3704
3705        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3706        :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise.
3707        """
3708        result = None
3709        msg = "Instrument not defined!"
3710
3711        if portfolio is None or not portfolio:
3712            portfolio = self.Overview(show=False)
3713
3714        if self._ticker:
3715            uLogger.debug("Searching instrument with ticker [{}] in opened positions...".format(self._ticker))
3716            msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker)
3717
3718            for iType in TKS_INSTRUMENTS:
3719                for instrument in portfolio["stat"][iType]:
3720                    if instrument["ticker"] == self._ticker:
3721                        result = instrument
3722                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self._ticker, instrument["figi"])
3723                        break
3724
3725        elif self._figi:
3726            uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self._figi))
3727            msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi)
3728
3729            for iType in TKS_INSTRUMENTS:
3730                for instrument in portfolio["stat"][iType]:
3731                    if instrument["figi"] == self._figi:
3732                        result = instrument
3733                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self._figi)
3734                        break
3735
3736        else:
3737            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3738
3739        uLogger.debug(msg)
3740
3741        return result

Returns instrument from the user's portfolio if it presents there. Instrument must be defined by ticker (highly priority) or figi.

Parameters
  • portfolio: dict with user's portfolio data. If None, then requests portfolio from Overview() method.
Returns

dict with instrument if portfolio contains open position with this instrument, None otherwise.

def IsInLimitOrders(self, portfolio: dict = None) -> bool:
3743    def IsInLimitOrders(self, portfolio: dict = None) -> bool:
3744        """
3745        Checks if instrument is in the limit orders list. Instrument must be defined by `ticker` (highly priority) or `figi`.
3746
3747        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3748
3749        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3750        :return: `True` if limit orders list contains some limit orders for the instrument, `False` otherwise.
3751        """
3752        result = False
3753        msg = "Instrument not defined!"
3754
3755        if portfolio is None or not portfolio:
3756            portfolio = self.Overview(show=False)
3757
3758        if self._ticker:
3759            uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker))
3760            msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker)
3761
3762            for instrument in portfolio["stat"]["orders"]:
3763                if instrument["ticker"] == self._ticker:
3764                    result = True
3765                    msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker)
3766                    break
3767
3768        elif self._figi:
3769            uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi))
3770            msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi)
3771
3772            for instrument in portfolio["stat"]["orders"]:
3773                if instrument["figi"] == self._figi:
3774                    result = True
3775                    msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi)
3776                    break
3777
3778        else:
3779            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3780
3781        uLogger.debug(msg)
3782
3783        return result

Checks if instrument is in the limit orders list. Instrument must be defined by ticker (highly priority) or figi.

See also: CloseAllByTicker() and CloseAllByFIGI().

Parameters
  • portfolio: dict with user's portfolio data. If None, then requests portfolio from Overview() method.
Returns

True if limit orders list contains some limit orders for the instrument, False otherwise.

def GetLimitOrderIDs(self, portfolio: dict = None) -> list[str]:
3785    def GetLimitOrderIDs(self, portfolio: dict = None) -> list[str]:
3786        """
3787        Returns list with all `orderID`s of opened pending limit orders for the instrument.
3788        Instrument must be defined by `ticker` (highly priority) or `figi`.
3789
3790        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3791
3792        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3793        :return: list with `orderID`s of limit orders.
3794        """
3795        result = []
3796        msg = "Instrument not defined!"
3797
3798        if portfolio is None or not portfolio:
3799            portfolio = self.Overview(show=False)
3800
3801        if self._ticker:
3802            uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker))
3803            msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker)
3804
3805            for instrument in portfolio["stat"]["orders"]:
3806                if instrument["ticker"] == self._ticker:
3807                    result.append(instrument["orderID"])
3808
3809            if result:
3810                msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker)
3811
3812        elif self._figi:
3813            uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi))
3814            msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi)
3815
3816            for instrument in portfolio["stat"]["orders"]:
3817                if instrument["figi"] == self._figi:
3818                    result.append(instrument["orderID"])
3819
3820            if result:
3821                msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi)
3822
3823        else:
3824            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3825
3826        uLogger.debug(msg)
3827
3828        return result

Returns list with all orderIDs of opened pending limit orders for the instrument. Instrument must be defined by ticker (highly priority) or figi.

See also: CloseAllByTicker() and CloseAllByFIGI().

Parameters
  • portfolio: dict with user's portfolio data. If None, then requests portfolio from Overview() method.
Returns

list with orderIDs of limit orders.

def IsInStopOrders(self, portfolio: dict = None) -> bool:
3830    def IsInStopOrders(self, portfolio: dict = None) -> bool:
3831        """
3832        Checks if instrument is in the stop orders list. Instrument must be defined by `ticker` (highly priority) or `figi`.
3833
3834        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3835
3836        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3837        :return: `True` if stop orders list contains some stop orders for the instrument, `False` otherwise.
3838        """
3839        result = False
3840        msg = "Instrument not defined!"
3841
3842        if portfolio is None or not portfolio:
3843            portfolio = self.Overview(show=False)
3844
3845        if self._ticker:
3846            uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker))
3847            msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker)
3848
3849            for instrument in portfolio["stat"]["stopOrders"]:
3850                if instrument["ticker"] == self._ticker:
3851                    result = True
3852                    msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker)
3853                    break
3854
3855        elif self._figi:
3856            uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi))
3857            msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi)
3858
3859            for instrument in portfolio["stat"]["stopOrders"]:
3860                if instrument["figi"] == self._figi:
3861                    result = True
3862                    msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi)
3863                    break
3864
3865        else:
3866            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3867
3868        uLogger.debug(msg)
3869
3870        return result

Checks if instrument is in the stop orders list. Instrument must be defined by ticker (highly priority) or figi.

See also: CloseAllByTicker() and CloseAllByFIGI().

Parameters
  • portfolio: dict with user's portfolio data. If None, then requests portfolio from Overview() method.
Returns

True if stop orders list contains some stop orders for the instrument, False otherwise.

def GetStopOrderIDs(self, portfolio: dict = None) -> list[str]:
3872    def GetStopOrderIDs(self, portfolio: dict = None) -> list[str]:
3873        """
3874        Returns list with all `orderID`s of opened stop orders for the instrument.
3875        Instrument must be defined by `ticker` (highly priority) or `figi`.
3876
3877        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3878
3879        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3880        :return: list with `orderID`s of stop orders.
3881        """
3882        result = []
3883        msg = "Instrument not defined!"
3884
3885        if portfolio is None or not portfolio:
3886            portfolio = self.Overview(show=False)
3887
3888        if self._ticker:
3889            uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker))
3890            msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker)
3891
3892            for instrument in portfolio["stat"]["stopOrders"]:
3893                if instrument["ticker"] == self._ticker:
3894                    result.append(instrument["orderID"])
3895
3896            if result:
3897                msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker)
3898
3899        elif self._figi:
3900            uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi))
3901            msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi)
3902
3903            for instrument in portfolio["stat"]["stopOrders"]:
3904                if instrument["figi"] == self._figi:
3905                    result.append(instrument["orderID"])
3906
3907            if result:
3908                msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi)
3909
3910        else:
3911            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3912
3913        uLogger.debug(msg)
3914
3915        return result

Returns list with all orderIDs of opened stop orders for the instrument. Instrument must be defined by ticker (highly priority) or figi.

See also: CloseAllByTicker() and CloseAllByFIGI().

Parameters
  • portfolio: dict with user's portfolio data. If None, then requests portfolio from Overview() method.
Returns

list with orderIDs of stop orders.

def RequestLimits(self) -> dict:
3917    def RequestLimits(self) -> dict:
3918        """
3919        Method for obtaining the available funds for withdrawal for current `accountId`.
3920
3921        See also:
3922        - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits
3923        - `OverviewLimits()` method
3924
3925        :return: dict with raw data from server that contains free funds for withdrawal. Example of dict:
3926                 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`.
3927                 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency
3928                 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures.
3929        """
3930        if self.accountId is None or not self.accountId:
3931            uLogger.error("Variable `accountId` must be defined for using this method!")
3932            raise Exception("Account ID required")
3933
3934        uLogger.debug("Requesting current available funds for withdrawal. Wait, please...")
3935
3936        self.body = str({"accountId": self.accountId})
3937        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits"
3938        rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
3939
3940        if self.moreDebug:
3941            uLogger.debug("Records about available funds for withdrawal successfully received")
3942
3943        return rawLimits

Method for obtaining the available funds for withdrawal for current accountId.

See also:

Returns

dict with raw data from server that contains free funds for withdrawal. Example of dict: {"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}. Here money is an array of portfolio currency positions, blocked is an array of blocked currency positions of the portfolio and blockedGuarantee is locked money under collateral for futures.

def OverviewLimits(self, show: bool = False, onlyFiles=False) -> dict:
3945    def OverviewLimits(self, show: bool = False, onlyFiles=False) -> dict:
3946        """
3947        Method for parsing and show table with available funds for withdrawal for current `accountId`.
3948
3949        See also: `RequestLimits()`.
3950
3951        :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log.
3952        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
3953        :return: dict with raw parsed data from server and some calculated statistics about it.
3954        """
3955        if self.accountId is None or not self.accountId:
3956            uLogger.error("Variable `accountId` must be defined for using this method!")
3957            raise Exception("Account ID required")
3958
3959        rawLimits = self.RequestLimits()  # raw response with current available funds for withdrawal
3960
3961        view = {
3962            "rawLimits": rawLimits,
3963            "limits": {  # parsed data for every currency:
3964                "money": {  # this is an array of portfolio currency positions
3965                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"]
3966                },
3967                "blocked": {  # this is an array of blocked currency
3968                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"]
3969                },
3970                "blockedGuarantee": {  # this is locked money under collateral for futures
3971                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"]
3972                },
3973            },
3974        }
3975
3976        # --- Prepare text table with limits in human-readable format:
3977        if show or onlyFiles:
3978            info = [
3979                "# Withdrawal limits\n\n",
3980                "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
3981                "* **Account ID:** [{}]\n".format(self.accountId),
3982            ]
3983
3984            if view["limits"]["money"]:
3985                info.extend([
3986                    "\n| Currencies | Total         | Available for withdrawal | Blocked for trade | Futures guarantee |\n",
3987                    "|------------|---------------|--------------------------|-------------------|-------------------|\n",
3988                ])
3989
3990            else:
3991                info.append("\nNo withdrawal limits\n")
3992
3993            for curr in view["limits"]["money"].keys():
3994                blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0
3995                blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0
3996                availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee)
3997
3998                infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format(
3999                    "[{}]".format(curr),
4000                    "{:.2f}".format(view["limits"]["money"][curr]),
4001                    "{:.2f}".format(availableMoney),
4002                    "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—",
4003                    "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—",
4004                )
4005
4006                if curr == "rub":
4007                    info.insert(5, infoStr)  # hack: insert "rub" at the first position in table and after headers
4008
4009                else:
4010                    info.append(infoStr)
4011
4012            infoText = "".join(info)
4013
4014            if show and not onlyFiles:
4015                uLogger.info(infoText)
4016
4017            if self.withdrawalLimitsFile and (show or onlyFiles):
4018                with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH:
4019                    fH.write(infoText)
4020
4021                uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile)))
4022
4023                if self.useHTMLReports:
4024                    htmlFilePath = self.withdrawalLimitsFile.replace(".md", ".html") if self.withdrawalLimitsFile.endswith(".md") else self.withdrawalLimitsFile + ".html"
4025                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
4026                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Withdrawal limits", commonCSS=COMMON_CSS, markdown=infoText))
4027
4028                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
4029
4030        return view

Method for parsing and show table with available funds for withdrawal for current accountId.

See also: RequestLimits().

Parameters
  • show: if False then only dictionary returns, if True then also print withdrawal limits to log.
  • onlyFiles: if True then do not show Markdown table in the console, but only generates report files.
Returns

dict with raw parsed data from server and some calculated statistics about it.

def RequestAccounts(self) -> dict:
4032    def RequestAccounts(self) -> dict:
4033        """
4034        Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`.
4035
4036        See also:
4037        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts
4038        - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account
4039        - `OverviewUserInfo()` method
4040
4041        :return: dict with raw data from server that contains accounts info. Example of dict:
4042                 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account",
4043                   "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z",
4044                   "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`.
4045                 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now.
4046        """
4047        uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...")
4048
4049        self.body = str({})
4050        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts"
4051        rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST")
4052
4053        if self.moreDebug:
4054            uLogger.debug("Records about available accounts successfully received")
4055
4056        return rawAccounts

Method for requesting all brokerage accounts (accountIds) of current user detected by token.

See also:

Returns

dict with raw data from server that contains accounts info. Example of dict: {"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account", "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z", "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}. If closedDate="1970-01-01T00:00:00Z" it means that account is active now.

def RequestUserInfo(self) -> dict:
4058    def RequestUserInfo(self) -> dict:
4059        """
4060        Method for requesting common user's information.
4061
4062        See also:
4063        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo
4064        - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest
4065        - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with
4066        - `OverviewUserInfo()` method
4067
4068        :return: dict with raw data from server that contains user's information. Example of dict:
4069                 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage",
4070                   "russian_shares", "structured_income_bonds"], "tariff": "premium"}`.
4071        """
4072        uLogger.debug("Requesting common user's information. Wait, please...")
4073
4074        self.body = str({})
4075        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo"
4076        rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST")
4077
4078        if self.moreDebug:
4079            uLogger.debug("Records about current user successfully received")
4080
4081        return rawUserInfo

Method for requesting common user's information.

See also:

Returns

dict with raw data from server that contains user's information. Example of dict: {"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage", "russian_shares", "structured_income_bonds"], "tariff": "premium"}.

def RequestMarginStatus(self, accountId: str = None) -> dict:
4083    def RequestMarginStatus(self, accountId: str = None) -> dict:
4084        """
4085        Method for requesting margin calculation for defined account ID.
4086
4087        See also:
4088        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes
4089        - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse
4090        - `OverviewUserInfo()` method
4091
4092        :param accountId: string with numeric account ID. If `None`, then used class field `accountId`.
4093        :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict.
4094                 Example of responses:
4095                 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`.
4096                 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000},
4097                                    "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000},
4098                                    "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000},
4099                                    "fundsSufficiencyLevel": {"units": "1", "nano": 280000000},
4100                                    "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`.
4101        """
4102        if accountId is None or not accountId:
4103            if self.accountId is None or not self.accountId:
4104                uLogger.error("Variable `accountId` must be defined for using this method!")
4105                raise Exception("Account ID required")
4106
4107            else:
4108                accountId = self.accountId  # use `self.accountId` (main ID) by default
4109
4110        uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId))
4111
4112        self.body = str({"accountId": accountId})
4113        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes"
4114        rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST")
4115
4116        if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}:
4117            uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId))
4118            rawMargin = {}
4119
4120        else:
4121            if self.moreDebug:
4122                uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId))
4123
4124        return rawMargin

Method for requesting margin calculation for defined account ID.

See also:

Parameters
  • accountId: string with numeric account ID. If None, then used class field accountId.
Returns

dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict. Example of responses: status code 400: {"code": 3, "message": "account margin status is disabled", "description": "30051" }, returns: {}. status code 200: {"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000}, "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000}, "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000}, "fundsSufficiencyLevel": {"units": "1", "nano": 280000000}, "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}.

def RequestTariffLimits(self) -> dict:
4126    def RequestTariffLimits(self) -> dict:
4127        """
4128        Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`.
4129
4130        See also:
4131        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff
4132        - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest
4133        - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit
4134        - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit
4135        - `OverviewUserInfo()` method
4136
4137        :return: dict with raw data from server that contains limits of current tariff. Example of dict:
4138                 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...],
4139                   "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`.
4140        """
4141        uLogger.debug("Requesting limits of current tariff. Wait, please...")
4142
4143        self.body = str({})
4144        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff"
4145        rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
4146
4147        if self.moreDebug:
4148            uLogger.debug("Records with limits of current tariff successfully received")
4149
4150        return rawTariffLimits

Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by token.

See also:

Returns

dict with raw data from server that contains limits of current tariff. Example of dict: {"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...], "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}.

def RequestBondCoupons(self, iJSON: dict) -> dict:
4152    def RequestBondCoupons(self, iJSON: dict) -> dict:
4153        """
4154        Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown
4155        then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`.
4156        All dates are in UTC timezone.
4157
4158        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons
4159        Documentation:
4160        - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest
4161        - response: https://tinkoff.github.io/investAPI/instruments/#coupon
4162
4163        See also: `ExtendBondsData()`.
4164
4165        :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self._ticker]`
4166                      If raw iJSON is not data of bond then server returns an error [400] with message:
4167                      `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`.
4168        :return: dictionary with bond payment calendar. Response example
4169                 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12",
4170                   "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000},
4171                   "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z",
4172                   "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}`
4173        """
4174        if iJSON["figi"] is None or not iJSON["figi"]:
4175            uLogger.error("FIGI must be defined for using this method!")
4176            raise Exception("FIGI required")
4177
4178        startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z"
4179        endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z"
4180
4181        uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format(
4182            "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "",
4183            self._figi,
4184            startDate,
4185            endDate,
4186        ))
4187
4188        self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate})
4189        calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons"
4190        calendar = self.SendAPIRequest(calendarURL, reqType="POST")
4191
4192        if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}:
4193            uLogger.warning("Instrument type is not bond!")
4194
4195        else:
4196            if self.moreDebug:
4197                uLogger.debug("Records about bond payment calendar successfully received")
4198
4199        return calendar

Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown then requesting dates "from": "1970-01-01T00:00:00.000Z" and "to": "2099-12-31T23:59:59.000Z". All dates are in UTC timezone.

REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons Documentation:

See also: ExtendBondsData().

Parameters
  • iJSON: raw json data of a bond from broker server, example iJSON = self.iList["Bonds"][self._ticker] If raw iJSON is not data of bond then server returns an error [400] with message: {"code": 3, "message": "instrument type is not bond", "description": "30048"}.
Returns

dictionary with bond payment calendar. Response example {"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12", "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000}, "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z", "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}

def ExtendBondsData( self, instruments: list[str], xlsx: bool = False) -> pandas.core.frame.DataFrame:
4201    def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame:
4202        """
4203        Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider
4204        Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar,
4205        coupon yields, current yields and some statistics etc.
4206
4207        WARNING! This is too long operation if a lot of bonds requested from broker server.
4208
4209        See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`.
4210
4211        :param instruments: list of strings with tickers or FIGIs.
4212        :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`,
4213                     for further used by data scientists or stock analytics.
4214        :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker.
4215                 In XLSX-file and Pandas DataFrame fields mean:
4216                 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond
4217                 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon
4218        """
4219        if instruments is None or not instruments:
4220            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
4221            raise Exception("Ticker or FIGI required")
4222
4223        if isinstance(instruments, str):
4224            instruments = [instruments]
4225
4226        uniqueInstruments = self.GetUniqueFIGIs(instruments)
4227
4228        uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...")
4229
4230        iCount = len(uniqueInstruments)
4231        tooLong = iCount >= 20
4232        if tooLong:
4233            uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...")
4234
4235        bonds = None
4236        for i, self._figi in enumerate(uniqueInstruments):
4237            instrument = self.SearchByFIGI(requestPrice=False)  # raw data about instrument from server
4238
4239            if "type" in instrument.keys() and instrument["type"] == "Bonds":
4240                # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond
4241                rawBond = self.SearchByFIGI(requestPrice=True)
4242
4243                # Widen raw data with UTC current time (iData["actualDateTime"]):
4244                actualDate = datetime.now(tzutc())
4245                iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond
4246
4247                # Widen raw data with bond payment calendar (iData["rawCalendar"]):
4248                iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)}
4249
4250                # Replace some values with human-readable:
4251                iData["nominalCurrency"] = iData["nominal"]["currency"]
4252                iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"])
4253                iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"])
4254                iData["aciCurrency"] = iData["aciValue"]["currency"]
4255                iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"])
4256                iData["issueSize"] = int(iData["issueSize"])
4257                iData["issueSizePlan"] = int(iData["issueSizePlan"])
4258                iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]]
4259                iData["step"] = iData["step"] if "step" in iData.keys() else 0
4260                iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]]
4261                iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0
4262                iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0
4263                iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0
4264                iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0
4265                iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0
4266                iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0
4267
4268                # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date):
4269                iData["limitUpPercent"] = iData["currentPrice"]["limitUp"]  # max price on current day in percents of nominal
4270                iData["limitDownPercent"] = iData["currentPrice"]["limitDown"]  # min price on current day in percents of nominal
4271                iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"]  # last price on market in percents of nominal
4272                iData["closePricePercent"] = iData["currentPrice"]["closePrice"]  # previous day close in percents of nominal
4273                iData["changes"] = iData["currentPrice"]["changes"]  # this is percent of changes between `currentPrice` and `lastPrice`
4274                iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100  # max price on current day is `limitUpPercent` * `nominal`
4275                iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100  # min price on current day is `limitDownPercent` * `nominal`
4276                iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100  # last price on market is `lastPricePercent` * `nominal`
4277                iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100  # previous day close is `closePricePercent` * `nominal`
4278                iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"]  # this is delta between last deal price and last close
4279
4280                # Widen raw data with calendar data from `rawCalendar` values:
4281                calendarData = []
4282                if "events" in iData["rawCalendar"].keys():
4283                    for item in iData["rawCalendar"]["events"]:
4284                        calendarData.append({
4285                            "couponDate": item["couponDate"],
4286                            "couponNumber": int(item["couponNumber"]),
4287                            "fixDate": item["fixDate"] if "fixDate" in item.keys() else "",
4288                            "payCurrency": item["payOneBond"]["currency"],
4289                            "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]),
4290                            "couponType": TKS_COUPON_TYPES[item["couponType"]],
4291                            "couponStartDate": item["couponStartDate"],
4292                            "couponEndDate": item["couponEndDate"],
4293                            "couponPeriod": item["couponPeriod"],
4294                        })
4295
4296                    # if maturity date is unknown then uses the latest date in bond payment calendar for it:
4297                    if "maturityDate" not in iData.keys():
4298                        iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else ""
4299
4300                # Widen raw data with Coupon Rate.
4301                # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%:
4302                iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData])
4303                iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData])
4304                iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0.
4305
4306                # Widen raw data with Yield to Maturity (YTM) on current date.
4307                # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%:
4308                maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None
4309                iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None
4310                iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate])
4311                iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"]  # sum of all last coupons minus current ACI value
4312                iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0.
4313
4314                iData["calendar"] = calendarData  # adds calendar at the end
4315
4316                # Remove not used data:
4317                iData.pop("uid")
4318                iData.pop("positionUid")
4319                iData.pop("currentPrice")
4320                iData.pop("rawCalendar")
4321
4322                colNames = list(iData.keys())
4323                if bonds is None:
4324                    bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames))
4325
4326                else:
4327                    bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True)
4328
4329            else:
4330                uLogger.warning("Instrument is not a bond!")
4331
4332            processed = round(100 * (i + 1) / iCount, 1)
4333            if tooLong and processed % 5 == 0:
4334                uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount))
4335
4336            else:
4337                uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount))
4338
4339        bonds.index = bonds["ticker"].tolist()  # replace indexes with ticker names
4340
4341        # Saving bonds from Pandas DataFrame to XLSX sheet:
4342        if xlsx and self.bondsXLSXFile:
4343            with pd.ExcelWriter(
4344                    path=self.bondsXLSXFile,
4345                    date_format=TKS_DATE_FORMAT,
4346                    datetime_format=TKS_DATE_TIME_FORMAT,
4347                    mode="w",
4348            ) as writer:
4349                bonds.to_excel(
4350                    writer,
4351                    sheet_name="Extended bonds data",
4352                    index=True,
4353                    encoding="UTF-8",
4354                    freeze_panes=(1, 1),
4355                )  # saving as XLSX-file with freeze first row and column as headers
4356
4357            uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile)))
4358
4359        return bonds

Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc.

WARNING! This is too long operation if a lot of bonds requested from broker server.

See also: ShowInstrumentInfo(), CreateBondsCalendar(), ShowBondsCalendar(), RequestBondCoupons().

Parameters
  • instruments: list of strings with tickers or FIGIs.
  • xlsx: if True then also exports Pandas DataFrame to xlsx-file bondsXLSXFile, default ext-bonds.xlsx, for further used by data scientists or stock analytics.
Returns

wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker. In XLSX-file and Pandas DataFrame fields mean: - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon

def CreateBondsCalendar( self, extBonds: pandas.core.frame.DataFrame, xlsx: bool = False) -> pandas.core.frame.DataFrame:
4361    def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame:
4362        """
4363        Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default.
4364
4365        WARNING! This is too long operation if a lot of bonds requested from broker server.
4366
4367        See also: `ShowBondsCalendar()`, `ExtendBondsData()`.
4368
4369        :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
4370                        extended information about bonds: main info, current prices, bond payment calendar,
4371                        coupon yields, current yields and some statistics etc.
4372                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
4373        :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default,
4374                     for further used by data scientists or stock analytics.
4375        :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon
4376        """
4377        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4378            extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=False)
4379
4380        uLogger.debug("Generating bond payments calendar data. Wait, please...")
4381
4382        colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"]
4383        colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"]
4384        calendar = None
4385        for bond in extBonds.iterrows():
4386            for item in bond[1]["calendar"]:
4387                cData = {
4388                    "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()),
4389                    "couponDate": item["couponDate"],
4390                    "figi": bond[1]["figi"],
4391                    "ticker": bond[1]["ticker"],
4392                    "name": bond[1]["name"],
4393                    "couponNumber": item["couponNumber"],
4394                    "payOneBond": item["payOneBond"],
4395                    "payCurrency": item["payCurrency"],
4396                    "couponType": item["couponType"],
4397                    "couponPeriod": item["couponPeriod"],
4398                    "fixDate": item["fixDate"],
4399                    "couponStartDate": item["couponStartDate"],
4400                    "couponEndDate": item["couponEndDate"],
4401                }
4402
4403                if calendar is None:
4404                    calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID))
4405
4406                else:
4407                    calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True)
4408
4409        if calendar is not None:
4410            calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True)  # sort all payments for all bonds by payment date
4411
4412            # Saving calendar from Pandas DataFrame to XLSX sheet:
4413            if xlsx:
4414                xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx"
4415
4416                with pd.ExcelWriter(
4417                        path=xlsxCalendarFile,
4418                        date_format=TKS_DATE_FORMAT,
4419                        datetime_format=TKS_DATE_TIME_FORMAT,
4420                        mode="w",
4421                ) as writer:
4422                    humanReadable = calendar.copy(deep=True)
4423                    humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0])
4424                    humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0])
4425                    humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0])
4426                    humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0])
4427                    humanReadable.columns = colNames  # human-readable column names
4428
4429                    humanReadable.to_excel(
4430                        writer,
4431                        sheet_name="Bond payments calendar",
4432                        index=False,
4433                        encoding="UTF-8",
4434                        freeze_panes=(1, 2),
4435                    )  # saving as XLSX-file with freeze first row and column as headers
4436
4437                    del humanReadable  # release df in memory
4438
4439                uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile)))
4440
4441        return calendar

Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, calendar.xlsx by default.

WARNING! This is too long operation if a lot of bonds requested from broker server.

See also: ShowBondsCalendar(), ExtendBondsData().

Parameters
  • extBonds: Pandas DataFrame object returns by ExtendBondsData() method and contains extended information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc. If this parameter is None then used figi or ticker as bond name and then calculate ExtendBondsData().
  • xlsx: if True then also exports Pandas DataFrame to file calendarFile + ".xlsx", calendar.xlsx by default, for further used by data scientists or stock analytics.
Returns

Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon

def ShowBondsCalendar( self, extBonds: pandas.core.frame.DataFrame, show: bool = True, onlyFiles=False) -> str:
4443    def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True, onlyFiles=False) -> str:
4444        """
4445        Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond.
4446        Also, creates Markdown file with calendar data, `calendar.md` by default.
4447
4448        See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`.
4449
4450        :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
4451                        extended information about bonds: main info, current prices, bond payment calendar,
4452                        coupon yields, current yields and some statistics etc.
4453                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
4454        :param show: if `True` then also printing bonds payment calendar to the console,
4455                     otherwise save to file `calendarFile` only. `False` by default.
4456        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
4457        :return: multilines text in Markdown format with bonds payment calendar as a table.
4458        """
4459        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4460            extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=show or onlyFiles)
4461
4462        infoText = "# Bond payments calendar\n\n"
4463
4464        calendar = self.CreateBondsCalendar(extBonds, xlsx=show or onlyFiles)  # generate Pandas DataFrame with full calendar data
4465
4466        if not (calendar is None or calendar.empty):
4467            splitLine = "|       |                 |              |              |     |               |           |        |                   |\n"
4468
4469            info = [
4470                "* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4471                "| Paid  | Payment date    | FIGI         | Ticker       | No. | Value         | Type      | Period | End registry date |\n",
4472                "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n",
4473            ]
4474
4475            newMonth = False
4476            notOneBond = calendar["figi"].nunique() > 1
4477            for i, bond in enumerate(calendar.iterrows()):
4478                if newMonth and notOneBond:
4479                    info.append(splitLine)
4480
4481                info.append(
4482                    "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format(
4483                        "  √" if bond[1]["paid"] else "  —",
4484                        bond[1]["couponDate"].split("T")[0],
4485                        bond[1]["figi"],
4486                        bond[1]["ticker"],
4487                        bond[1]["couponNumber"],
4488                        "{} {}".format(
4489                            "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."),
4490                            bond[1]["payCurrency"],
4491                        ),
4492                        bond[1]["couponType"],
4493                        bond[1]["couponPeriod"],
4494                        bond[1]["fixDate"].split("T")[0],
4495                    )
4496                )
4497
4498                if i < len(calendar.values) - 1:
4499                    curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4500                    nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4501                    newMonth = False if curDate.month == nextDate.month else True
4502
4503                else:
4504                    newMonth = False
4505
4506            infoText += "".join(info)
4507
4508            if show and not onlyFiles:
4509                uLogger.info("{}".format(infoText))
4510
4511            if self.calendarFile is not None and (show or onlyFiles):
4512                with open(self.calendarFile, "w", encoding="UTF-8") as fH:
4513                    fH.write(infoText)
4514
4515                uLogger.info("Bond payments calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile)))
4516
4517                if self.useHTMLReports:
4518                    htmlFilePath = self.calendarFile.replace(".md", ".html") if self.calendarFile.endswith(".md") else self.calendarFile + ".html"
4519                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
4520                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Bond payments calendar", commonCSS=COMMON_CSS, markdown=infoText))
4521
4522                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
4523
4524        else:
4525            infoText += "No data\n"
4526
4527        return infoText

Show bond payments calendar as a table. One row in input bonds dataframe contains one bond. Also, creates Markdown file with calendar data, calendar.md by default.

See also: ShowInstrumentInfo(), RequestBondCoupons(), CreateBondsCalendar() and ExtendBondsData().

Parameters
  • extBonds: Pandas DataFrame object returns by ExtendBondsData() method and contains extended information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc. If this parameter is None then used figi or ticker as bond name and then calculate ExtendBondsData().
  • show: if True then also printing bonds payment calendar to the console, otherwise save to file calendarFile only. False by default.
  • onlyFiles: if True then do not show Markdown table in the console, but only generates report files.
Returns

multilines text in Markdown format with bonds payment calendar as a table.

def OverviewAccounts(self, show: bool = False, onlyFiles=False) -> dict:
4529    def OverviewAccounts(self, show: bool = False, onlyFiles=False) -> dict:
4530        """
4531        Method for parsing and show simple table with all available user accounts.
4532
4533        See also: `RequestAccounts()` and `OverviewUserInfo()` methods.
4534
4535        :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log.
4536        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
4537        :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict:
4538                 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...},
4539                          "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1",
4540                                                        "status": "Opened and active account", "opened": "2018-05-23 00:00:00",
4541                                                        "closed": "—", "access": "Full access" }, ...}}`
4542        """
4543        rawAccounts = self.RequestAccounts()  # Raw responses with accounts
4544
4545        # This is an array of dict with user accounts, its `accountId`s and some parsed data:
4546        accounts = {
4547            item["id"]: {
4548                "type": TKS_ACCOUNT_TYPES[item["type"]],
4549                "name": item["name"],
4550                "status": TKS_ACCOUNT_STATUSES[item["status"]],
4551                "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4552                "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—",
4553                "access": TKS_ACCESS_LEVELS[item["accessLevel"]],
4554            } for item in rawAccounts["accounts"]
4555        }
4556
4557        # Raw and parsed data with some fields replaced in "stat" section:
4558        view = {
4559            "rawAccounts": rawAccounts,
4560            "stat": accounts,
4561        }
4562
4563        # --- Prepare simple text table with only accounts data in human-readable format:
4564        if show or onlyFiles:
4565            info = [
4566                "# User accounts\n\n",
4567                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4568                "| Account ID   | Type                      | Status                    | Name                           |\n",
4569                "|--------------|---------------------------|---------------------------|--------------------------------|\n",
4570            ]
4571
4572            for account in view["stat"].keys():
4573                info.extend([
4574                    "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format(
4575                        account,
4576                        view["stat"][account]["type"],
4577                        view["stat"][account]["status"],
4578                        view["stat"][account]["name"],
4579                    )
4580                ])
4581
4582            infoText = "".join(info)
4583
4584            if show and not onlyFiles:
4585                uLogger.info(infoText)
4586
4587            if self.userAccountsFile and (show or onlyFiles):
4588                with open(self.userAccountsFile, "w", encoding="UTF-8") as fH:
4589                    fH.write(infoText)
4590
4591                uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile)))
4592
4593                if self.useHTMLReports:
4594                    htmlFilePath = self.userAccountsFile.replace(".md", ".html") if self.userAccountsFile.endswith(".md") else self.userAccountsFile + ".html"
4595                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
4596                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User accounts", commonCSS=COMMON_CSS, markdown=infoText))
4597
4598                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
4599
4600        return view

Method for parsing and show simple table with all available user accounts.

See also: RequestAccounts() and OverviewUserInfo() methods.

Parameters
  • show: if False then only dictionary with accounts data returns, if True then also print it to log.
  • onlyFiles: if True then do not show Markdown table in the console, but only generates report files.
Returns

dict with parsed accounts data received from RequestAccounts() method. Example of dict: view = {"rawAccounts": {rawAccounts from RequestAccounts() method...}, "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1", "status": "Opened and active account", "opened": "2018-05-23 00:00:00", "closed": "—", "access": "Full access" }, ...}}

def OverviewUserInfo(self, show: bool = False, onlyFiles=False) -> dict:
4602    def OverviewUserInfo(self, show: bool = False, onlyFiles=False) -> dict:
4603        """
4604        Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit).
4605
4606        See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods.
4607
4608        :param show: if `False` then only dictionary returns, if `True` then also print user's data to log.
4609        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
4610        :return: dict with raw parsed data from server and some calculated statistics about it.
4611        """
4612        overview = self.Overview(show=False)  # Request current user portfolio for the ability to calculate missing funds
4613        tmpTicker = self._ticker
4614        self._ticker = "RUB000UTSTOM"  # This instrument show in rub how much money cost current margin
4615        missing = self.GetInstrumentFromPortfolio(portfolio=overview)
4616        self._ticker = tmpTicker
4617
4618        rawUserInfo = self.RequestUserInfo()  # Raw response with common user info
4619        overviewAccount = self.OverviewAccounts(show=False)  # Raw and parsed accounts data
4620        rawAccounts = overviewAccount["rawAccounts"]  # Raw response with user accounts data
4621        accounts = overviewAccount["stat"]  # Dict with only statistics about user accounts
4622        rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()}  # Raw response with margin calculation for every account ID
4623        rawTariffLimits = self.RequestTariffLimits()  # Raw response with limits of current tariff
4624
4625        # This is dict with parsed common user data:
4626        userInfo = {
4627            "premium": "Yes" if rawUserInfo["premStatus"] else "No",
4628            "qualified": "Yes" if rawUserInfo["qualStatus"] else "No",
4629            "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]],
4630            "tariff": rawUserInfo["tariff"],
4631        }
4632
4633        # This is an array of dict with parsed margin statuses for every account IDs:
4634        margins = {}
4635        for accountId in accounts.keys():
4636            if rawMargins[accountId]:
4637                margins[accountId] = {
4638                    "currency": rawMargins[accountId]["liquidPortfolio"]["currency"],
4639                    "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]),
4640                    "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]),
4641                    "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]),
4642                    "diff": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]),
4643                    "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]),
4644                    "missing": missing["volume"],
4645                }
4646
4647            else:
4648                margins[accountId] = {}  # Server response: margin status is disabled for current accountId
4649
4650        unary = {}  # unary-connection limits
4651        for item in rawTariffLimits["unaryLimits"]:
4652            if item["limitPerMinute"] in unary.keys():
4653                unary[item["limitPerMinute"]].extend(item["methods"])
4654
4655            else:
4656                unary[item["limitPerMinute"]] = item["methods"]
4657
4658        stream = {}  # stream-connection limits
4659        for item in rawTariffLimits["streamLimits"]:
4660            if item["limit"] in stream.keys():
4661                stream[item["limit"]].extend(item["streams"])
4662
4663            else:
4664                stream[item["limit"]] = item["streams"]
4665
4666        # This is dict with parsed limits of current tariff (connections, API methods etc.):
4667        limits = {
4668            "unary": unary,
4669            "stream": stream,
4670        }
4671
4672        # Raw and parsed data as an output result:
4673        view = {
4674            "rawUserInfo": rawUserInfo,
4675            "rawAccounts": rawAccounts,
4676            "rawMargins": rawMargins,
4677            "rawTariffLimits": rawTariffLimits,
4678            "stat": {
4679                "overview": overview,
4680                "userInfo": userInfo,
4681                "accounts": accounts,
4682                "margins": margins,
4683                "limits": limits,
4684            },
4685        }
4686
4687        # --- Prepare text table with user information in human-readable format:
4688        if show or onlyFiles:
4689            info = [
4690                "# Full user information\n\n",
4691                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4692                "## Common information\n\n",
4693                "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]),
4694                "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]),
4695                "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]),
4696                "* **Allowed to work with instruments:**\n{}\n".format("".join(["  - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])),
4697                "\n## User accounts\n\n",
4698            ]
4699
4700            for account in view["stat"]["accounts"].keys():
4701                info.extend([
4702                    "### ID: [{}]\n\n".format(account),
4703                    "| Parameters           | Values                                                       |\n",
4704                    "|----------------------|--------------------------------------------------------------|\n",
4705                    "| Account type:        | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]),
4706                    "| Account name:        | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]),
4707                    "| Account status:      | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]),
4708                    "| Access level:        | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]),
4709                    "| Date opened:         | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]),
4710                    "| Date closed:         | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]),
4711                ])
4712
4713                if margins[account]:
4714                    info.extend([
4715                        "| Margin status:       | Enabled                                                      |\n",
4716                        "| - Liquid portfolio:  | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])),
4717                        "| - Margin starting:   | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])),
4718                        "| - Margin minimum:    | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])),
4719                        "| - Margin difference: | {:<60} |\n".format("{} {}".format(margins[account]["diff"], margins[account]["currency"])),
4720                        "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)),
4721                        "| - Not covered funds: | {:<60} |\n\n".format("{:.2f} {}".format(margins[account]["missing"], margins[account]["currency"])),
4722                    ])
4723
4724                else:
4725                    info.append("| Margin status:       | Disabled                                                     |\n\n")
4726
4727            info.extend([
4728                "\n## Current user tariff limits\n",
4729                "\n### See also\n",
4730                "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n",
4731                "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n",
4732                "  - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n",
4733                "  - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n",
4734                "\n### Unary limits\n",
4735            ])
4736
4737            if unary:
4738                for key, values in sorted(unary.items()):
4739                    info.append("\n* Max requests per minute: {}\n".format(key))
4740
4741                    for value in values:
4742                        info.append("  - {}\n".format(value))
4743
4744            else:
4745                info.append("\nNot available\n")
4746
4747            info.append("\n### Stream limits\n")
4748
4749            if stream:
4750                for key, values in sorted(stream.items()):
4751                    info.append("\n* Max stream connections: {}\n".format(key))
4752
4753                    for value in values:
4754                        info.append("  - {}\n".format(value))
4755
4756            else:
4757                info.append("\nNot available\n")
4758
4759            infoText = "".join(info)
4760
4761            if show and not onlyFiles:
4762                uLogger.info(infoText)
4763
4764            if self.userInfoFile and (show or onlyFiles):
4765                with open(self.userInfoFile, "w", encoding="UTF-8") as fH:
4766                    fH.write(infoText)
4767
4768                uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile)))
4769
4770                if self.useHTMLReports:
4771                    htmlFilePath = self.userInfoFile.replace(".md", ".html") if self.userInfoFile.endswith(".md") else self.userInfoFile + ".html"
4772                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
4773                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User info", commonCSS=COMMON_CSS, markdown=infoText))
4774
4775                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
4776
4777        return view

Method for parsing and show all available user's data (accountIds, common user information, margin status and tariff connections limit).

See also: OverviewAccounts(), RequestAccounts(), RequestUserInfo(), RequestMarginStatus() and RequestTariffLimits() methods.

Parameters
  • show: if False then only dictionary returns, if True then also print user's data to log.
  • onlyFiles: if True then do not show Markdown table in the console, but only generates report files.
Returns

dict with raw parsed data from server and some calculated statistics about it.

class Args:
4780class Args:
4781    """
4782    If `Main()` function is imported as module, then this class used to convert arguments from **kwargs as object.
4783    """
4784    def __init__(self, **kwargs):
4785        self.__dict__.update(kwargs)
4786
4787    def __getattr__(self, item):
4788        return None

If Main() function is imported as module, then this class used to convert arguments from **kwargs as object.

Args(**kwargs)
4784    def __init__(self, **kwargs):
4785        self.__dict__.update(kwargs)
def ParseArgs():
4791def ParseArgs():
4792    """This function get and parse command line keys."""
4793    parser = ArgumentParser()  # command-line string parser
4794
4795    parser.description = "TKSBrokerAPI is a trading platform for automation on Python to simplify the implementation of trading scenarios and work with Tinkoff Invest API server via the REST protocol. See examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md"
4796    parser.usage = "\n/as module/ python TKSBrokerAPI.py [some options] [one command]\n/as CLI tool/ tksbrokerapi [some options] [one command]"
4797
4798    # --- options:
4799
4800    parser.add_argument("--no-cache", action="store_true", default=False, help="Option: not use local cache `dump.json`, but update raw instruments data when starting the platform. `False` by default.")
4801    parser.add_argument("--token", type=str, help="Option: Tinkoff service's api key. If not set then used environment variable `TKS_API_TOKEN`. See how to use: https://tinkoff.github.io/investAPI/token/")
4802    parser.add_argument("--account-id", type=str, default=None, help="Option: string with an user numeric account ID in Tinkoff Broker. It can be found in any broker's reports (see the contract number). Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.")
4803
4804    parser.add_argument("--ticker", "-t", type=str, help="Option: instrument's ticker, e.g. `IBM`, `YNDX`, `GOOGL` etc. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR`.")
4805    parser.add_argument("--figi", "-f", type=str, help="Option: instrument's FIGI, e.g. `BBG006L8G4H1` (for `YNDX`).")
4806
4807    parser.add_argument("--depth", type=int, default=1, help="Option: Depth of Market (DOM) can be >=1, 1 by default.")
4808    parser.add_argument("--no-cancelled", "--no-canceled", action="store_true", default=False, help="Option: remove information about cancelled operations from the deals report by the `--deals` key. `False` by default.")
4809
4810    parser.add_argument("--output", type=str, default=None, help="Option: replace default paths to output files for some commands. If `None` then used default files.")
4811    parser.add_argument("--html", "--HTML", action="store_true", default=False, help="Option: if key present then TKSBrokerAPI generate also HTML reports from Markdown. False by default.")
4812
4813    parser.add_argument("--interval", type=str, default="hour", help="Option: available values are `1min`, `5min`, `15min`, `hour` and `day`. Used only with `--history` key. This is time period of one candle. Default: `hour` for every history candles.")
4814    parser.add_argument("--only-missing", action="store_true", default=False, help="Option: if history file define by `--output` key then add only last missing candles, do not request all history length. `False` by default.")
4815    parser.add_argument("--csv-sep", type=str, default=",", help="Option: separator if csv-file is used, `,` by default.")
4816
4817    parser.add_argument("--debug-level", "--log-level", "--verbosity", "-v", type=int, default=20, help="Option: showing STDOUT messages of minimal debug level, e.g. 10 = DEBUG, 20 = INFO, 30 = WARNING, 40 = ERROR, 50 = CRITICAL. INFO (20) by default.")
4818    parser.add_argument("--more", "--more-debug", action="store_true", default=False, help="Option: `--debug-level` key only switch log level verbosity, but in addition `--more` key enable all debug information, such as net request and response headers in all methods.")
4819    parser.add_argument("--tag", type=str, default="", help="Option: identification TKSBrokerAPI tag in log messages to simplify debugging when platform instances runs in parallel mode. Default: `""` (empty string).")
4820
4821    # --- commands:
4822
4823    parser.add_argument("--version", "--ver", action="store_true", help="Action: shows current semantic version, looks like `major.minor.buildnumber`. If TKSBrokerAPI not installed via pip, then used local build number `.dev0`.")
4824
4825    parser.add_argument("--list", "-l", action="store_true", help="Action: get and print all available instruments and some information from broker server. Also, you can define `--output` key to save list of instruments to file, default: `instruments.md`.")
4826    parser.add_argument("--list-xlsx", "-x", action="store_true", help="Action: get all available instruments from server for current account and save raw data into xlsx-file for further used by data scientists or stock analytics, default: `dump.xlsx`.")
4827    parser.add_argument("--bonds-xlsx", "-b", type=str, nargs="*", help="Action: get all available bonds if only key present or list of bonds with FIGIs or tickers and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bonds payment calendar, coupon yields, current yields and some statistics etc. And then export data to XLSX-file, default: `ext-bonds.xlsx` or you can change it with `--output` key. WARNING! This is too long operation if a lot of bonds requested from broker server.")
4828    parser.add_argument("--search", "-s", type=str, nargs=1, help="Action: search for an instruments by part of the name, ticker or FIGI. Also, you can define `--output` key to save results to file, default: `search-results.md`.")
4829    parser.add_argument("--info", "-i", action="store_true", help="Action: get information from broker server about instrument by it's ticker or FIGI. `--ticker` key or `--figi` key must be defined!")
4830    parser.add_argument("--calendar", "-c", type=str, nargs="*", help="Action: show bonds payment calendar as a table. Calendar build for one or more tickers or FIGIs, or for all bonds if only key present. If the `--output` key present then calendar saves to file, default: `calendar.md`. Also, created XLSX-file with bond payments calendar for further used by data scientists or stock analytics, `calendar.xlsx` by default. WARNING! This is too long operation if a lot of bonds requested from broker server.")
4831    parser.add_argument("--price", action="store_true", help="Action: show actual price list for current instrument. Also, you can use `--depth` key. `--ticker` key or `--figi` key must be defined!")
4832    parser.add_argument("--prices", "-p", type=str, nargs="+", help="Action: get and print current prices for list of given instruments (by it's tickers or by FIGIs). WARNING! This is too long operation if you request a lot of instruments! Also, you can define `--output` key to save list of prices to file, default: `prices.md`.")
4833
4834    parser.add_argument("--overview", "-o", action="store_true", help="Action: shows all open positions, orders and some statistics. Also, you can define `--output` key to save this information to file, default: `overview.md`.")
4835    parser.add_argument("--overview-digest", action="store_true", help="Action: shows a short digest of the portfolio status. Also, you can define `--output` key to save this information to file, default: `overview-digest.md`.")
4836    parser.add_argument("--overview-positions", action="store_true", help="Action: shows only open positions. Also, you can define `--output` key to save this information to file, default: `overview-positions.md`.")
4837    parser.add_argument("--overview-orders", action="store_true", help="Action: shows only sections of open limits and stop orders. Also, you can define `--output` key to save orders to file, default: `overview-orders.md`.")
4838    parser.add_argument("--overview-analytics", action="store_true", help="Action: shows only the analytics section and the distribution of the portfolio by various categories. Also, you can define `--output` key to save this information to file, default: `overview-analytics.md`.")
4839    parser.add_argument("--overview-calendar", action="store_true", help="Action: shows only the bonds calendar section (if these present in portfolio). Also, you can define `--output` key to save this information to file, default: `overview-calendar.md`.")
4840
4841    parser.add_argument("--deals", "-d", type=str, nargs="*", help="Action: show all deals between two given dates. Start day may be an integer number: -1, -2, -3 days ago. Also, you can use keywords: `today`, `yesterday` (-1), `week` (-7), `month` (-30) and `year` (-365). Dates format must be: `%%Y-%%m-%%d`, e.g. 2020-02-03. With `--no-cancelled` key information about cancelled operations will be removed from the deals report. Also, you can define `--output` key to save all deals to file, default: `deals.md`.")
4842    parser.add_argument("--history", type=str, nargs="*", help="Action: get last history candles of the current instrument defined by `--ticker` or `--figi` (FIGI id) keys. History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. This action may be used together with the `--render-chart` key. Also, you can define `--output` key to save history candlesticks to file.")
4843    parser.add_argument("--load-history", type=str, help="Action: try to load history candles from given csv-file as a Pandas Dataframe and print it in to the console. This action may be used together with the `--render-chart` key.")
4844    parser.add_argument("--render-chart", type=str, help="Action: render candlesticks chart. This key may only used with `--history` or `--load-history` together. Action has 1 parameter with two possible string values: `interact` (`i`) or `non-interact` (`ni`).")
4845
4846    parser.add_argument("--trade", nargs="*", help="Action: universal action to open market position for defined ticker or FIGI. You must specify 1-5 parameters: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. See examples in readme.")
4847    parser.add_argument("--buy", nargs="*", help="Action: immediately open BUY market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].")
4848    parser.add_argument("--sell", nargs="*", help="Action: immediately open SELL market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].")
4849
4850    parser.add_argument("--order", nargs="*", help="Action: universal action to open limit or stop-order in any directions. You must specify 4-7 parameters: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]]. See examples in readme.")
4851    parser.add_argument("--buy-limit", type=float, nargs=2, help="Action: open pending BUY limit-order (below current price). You must specify only 2 parameters: [lots] [target price] to open BUY limit-order. If you try to create `Buy` limit-order above current price then broker immediately open `Buy` market order, such as if you do simple `--buy` operation!")
4852    parser.add_argument("--sell-limit", type=float, nargs=2, help="Action: open pending SELL limit-order (above current price). You must specify only 2 parameters: [lots] [target price] to open SELL limit-order. If you try to create `Sell` limit-order below current price then broker immediately open `Sell` market order, such as if you do simple `--sell` operation!")
4853    parser.add_argument("--buy-stop", nargs="*", help="Action: open BUY stop-order. You must specify at least 2 parameters: [lots] [target price] to open BUY stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.")
4854    parser.add_argument("--sell-stop", nargs="*", help="Action: open SELL stop-order. You must specify at least 2 parameters: [lots] [target price] to open SELL stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.")
4855    # parser.add_argument("--buy-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending BUY limit-orders (below current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!")
4856    # parser.add_argument("--sell-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending SELL limit-orders (above current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!")
4857
4858    parser.add_argument("--close-order", "--cancel-order", type=str, nargs=1, help="Action: close only one order by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.")
4859    parser.add_argument("--close-orders", "--cancel-orders", type=str, nargs="+", help="Action: close one or list of orders by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.")
4860    parser.add_argument("--close-trade", "--cancel-trade", action="store_true", help="Action: close only one position for instrument defined by `--ticker` (high priority) or `--figi` keys, including for currencies tickers.")
4861    parser.add_argument("--close-trades", "--cancel-trades", type=str, nargs="+", help="Action: close positions for list of tickers or FIGIs, including for currencies tickers or FIGIs.")
4862    parser.add_argument("--close-all", "--cancel-all", type=str, nargs="*", help="Action: close all available (not blocked) opened trades and orders, excluding for currencies. Also you can select one or more keywords case insensitive to specify trades type: `orders`, `shares`, `bonds`, `etfs` and `futures`, but not `currencies`. Currency positions you must closes manually using `--buy`, `--sell`, `--close-trade` or `--close-trades` operations. If the `--close-all` key present with the `--ticker` or `--figi` keys, then positions and all open limit and stop orders for the specified instrument are closed.")
4863
4864    parser.add_argument("--limits", "--withdrawal-limits", "-w", action="store_true", help="Action: show table of funds available for withdrawal for current `accountId`. You can change `accountId` with the key `--account-id`. Also, you can define `--output` key to save this information to file, default: `limits.md`.")
4865    parser.add_argument("--user-info", "-u", action="store_true", help="Action: show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). Also, you can define `--output` key to save this information to file, default: `user-info.md`.")
4866    parser.add_argument("--account", "--accounts", "-a", action="store_true", help="Action: show simple table with all available user accounts. Also, you can define `--output` key to save this information to file, default: `accounts.md`.")
4867
4868    cmdArgs = parser.parse_args()
4869    return cmdArgs

This function get and parse command line keys.

def Main(**kwargs):
4872def Main(**kwargs):
4873    """
4874    Main function for work with TKSBrokerAPI in the console.
4875
4876    See examples:
4877    - in english: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md
4878    - in russian: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README.md
4879    """
4880    args = Args(**kwargs) if kwargs else ParseArgs()  # get and parse command-line parameters or use **kwarg parameters
4881
4882    if args.debug_level:
4883        uLogger.level = 10  # always debug level by default
4884        uLogger.handlers[0].level = args.debug_level  # level for STDOUT
4885
4886    exitCode = 0
4887    start = datetime.now(tzutc())
4888    uLogger.debug("=-" * 50)
4889    uLogger.debug(">>> TKSBrokerAPI module started at: [{}] UTC, it is [{}] local time".format(
4890        start.strftime(TKS_PRINT_DATE_TIME_FORMAT),
4891        start.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4892    ))
4893
4894    # trying to calculate full current version:
4895    buildVersion = __version__
4896    try:
4897        v = version("tksbrokerapi")
4898        buildVersion = v if v.startswith(buildVersion) else buildVersion + ".dev0"  # set version as major.minor.dev0 if run as local build or local script
4899
4900    except Exception:
4901        buildVersion = __version__ + ".dev0"  # if an errors occurred then also set version as major.minor.dev0
4902
4903    uLogger.debug("TKSBrokerAPI major.minor.build version used: [{}]".format(buildVersion))
4904    uLogger.debug("Host CPU count: [{}]".format(CPU_COUNT))
4905
4906    try:
4907        if args.version:
4908            print("TKSBrokerAPI {}".format(buildVersion))
4909            uLogger.debug("User requested current TKSBrokerAPI major.minor.build version: [{}]".format(buildVersion))
4910
4911        else:
4912            # Init class for trading with Tinkoff Broker:
4913            trader = TinkoffBrokerServer(
4914                token=args.token,
4915                accountId=args.account_id,
4916                useCache=not args.no_cache,
4917            )
4918
4919            if args.tag is not None:
4920                trader.tag = args.tag  # Identification TKSBrokerAPI tag in log messages to simplify debugging when platform instances runs in parallel mode
4921
4922            # --- set some options:
4923
4924            if args.more:
4925                trader.moreDebug = True
4926                uLogger.warning("More debug info mode is enabled! See network requests, responses and its headers in the full log or run TKSBrokerAPI platform with the `--verbosity 10` to show theres in console.")
4927
4928            if args.html:
4929                trader.useHTMLReports = True
4930
4931            if args.ticker:
4932                ticker = str(args.ticker).upper()  # Tickers may be upper case only
4933
4934                if ticker in trader.aliasesKeys:
4935                    trader.ticker = trader.aliases[ticker]  # Replace some tickers with its aliases
4936
4937                else:
4938                    trader.ticker = ticker
4939
4940            if args.figi:
4941                trader.figi = str(args.figi).upper()  # FIGIs may be upper case only
4942
4943            if args.depth is not None:
4944                trader.depth = args.depth
4945
4946            # --- do one command:
4947
4948            if args.list:
4949                if args.output is not None:
4950                    trader.instrumentsFile = args.output
4951
4952                trader.ShowInstrumentsInfo(show=True)
4953
4954            elif args.list_xlsx:
4955                trader.DumpInstrumentsAsXLSX(forceUpdate=False)
4956
4957            elif args.bonds_xlsx is not None:
4958                if args.output is not None:
4959                    trader.bondsXLSXFile = args.output
4960
4961                if len(args.bonds_xlsx) == 0:
4962                    trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=True)  # request bonds with all available tickers
4963
4964                else:
4965                    trader.ExtendBondsData(instruments=args.bonds_xlsx, xlsx=True)  # request list of given bonds
4966
4967            elif args.search:
4968                if args.output is not None:
4969                    trader.searchResultsFile = args.output
4970
4971                trader.SearchInstruments(pattern=args.search[0], show=True)
4972
4973            elif args.info:
4974                if not (args.ticker or args.figi):
4975                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
4976                    raise Exception("Ticker or FIGI required")
4977
4978                if args.output is not None:
4979                    trader.infoFile = args.output
4980
4981                if args.ticker:
4982                    trader.SearchByTicker(requestPrice=True, show=True)  # show info and current prices by ticker name
4983
4984                else:
4985                    trader.SearchByFIGI(requestPrice=True, show=True)  # show info and current prices by FIGI id
4986
4987            elif args.calendar is not None:
4988                if args.output is not None:
4989                    trader.calendarFile = args.output
4990
4991                if len(args.calendar) == 0:
4992                    bondsData = trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=False)  # request bonds with all available tickers
4993
4994                else:
4995                    bondsData = trader.ExtendBondsData(instruments=args.calendar, xlsx=False)  # request list of given bonds
4996
4997                trader.ShowBondsCalendar(extBonds=bondsData, show=True)  # shows bonds payment calendar only
4998
4999            elif args.price:
5000                if not (args.ticker or args.figi):
5001                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
5002                    raise Exception("Ticker or FIGI required")
5003
5004                trader.GetCurrentPrices(show=True)
5005
5006            elif args.prices is not None:
5007                if args.output is not None:
5008                    trader.pricesFile = args.output
5009
5010                trader.GetListOfPrices(instruments=args.prices, show=True)  # WARNING: too long wait for a lot of instruments prices
5011
5012            elif args.overview:
5013                if args.output is not None:
5014                    trader.overviewFile = args.output
5015
5016                trader.Overview(show=True, details="full")
5017
5018            elif args.overview_digest:
5019                if args.output is not None:
5020                    trader.overviewDigestFile = args.output
5021
5022                trader.Overview(show=True, details="digest")
5023
5024            elif args.overview_positions:
5025                if args.output is not None:
5026                    trader.overviewPositionsFile = args.output
5027
5028                trader.Overview(show=True, details="positions")
5029
5030            elif args.overview_orders:
5031                if args.output is not None:
5032                    trader.overviewOrdersFile = args.output
5033
5034                trader.Overview(show=True, details="orders")
5035
5036            elif args.overview_analytics:
5037                if args.output is not None:
5038                    trader.overviewAnalyticsFile = args.output
5039
5040                trader.Overview(show=True, details="analytics")
5041
5042            elif args.overview_calendar:
5043                if args.output is not None:
5044                    trader.overviewAnalyticsFile = args.output
5045
5046                trader.Overview(show=True, details="calendar")
5047
5048            elif args.deals is not None:
5049                if args.output is not None:
5050                    trader.reportFile = args.output
5051
5052                if 0 <= len(args.deals) < 3:
5053                    trader.Deals(
5054                        start=args.deals[0] if len(args.deals) >= 1 else None,
5055                        end=args.deals[1] if len(args.deals) == 2 else None,
5056                        show=True,  # Always show deals report in console
5057                        showCancelled=not args.no_cancelled,  # If --no-cancelled key then remove cancelled operations from the deals report. False by default.
5058                    )
5059
5060                else:
5061                    uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]")
5062                    raise Exception("Incorrect value")
5063
5064            elif args.history is not None:
5065                if args.output is not None:
5066                    trader.historyFile = args.output
5067
5068                if 0 <= len(args.history) < 3:
5069                    dataReceived = trader.History(
5070                        start=args.history[0] if len(args.history) >= 1 else None,
5071                        end=args.history[1] if len(args.history) == 2 else None,
5072                        interval="hour" if args.interval is None or not args.interval else args.interval,
5073                        onlyMissing=False if args.only_missing is None or not args.only_missing else args.only_missing,
5074                        csvSep="," if args.csv_sep is None or not args.csv_sep else args.csv_sep,
5075                        show=True,  # shows all downloaded candles in console
5076                    )
5077
5078                    if args.render_chart is not None and dataReceived is not None:
5079                        iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True
5080
5081                        trader.ShowHistoryChart(
5082                            candles=dataReceived,
5083                            interact=iChart,
5084                            openInBrowser=False,  # False by default, to avoid issues with `permissions denied` to html-file.
5085                        )
5086
5087                else:
5088                    uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]")
5089                    raise Exception("Incorrect value")
5090
5091            elif args.load_history is not None:
5092                histData = trader.LoadHistory(filePath=args.load_history)  # load data from file and show history in console
5093
5094                if args.render_chart is not None and histData is not None:
5095                    iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True
5096                    trader.ticker = os.path.basename(args.load_history)  # use filename as ticker name for PriceGenerator's chart
5097
5098                    trader.ShowHistoryChart(
5099                        candles=histData,
5100                        interact=iChart,
5101                        openInBrowser=False,  # False by default, to avoid issues with `permissions denied` to html-file.
5102                    )
5103
5104            elif args.trade is not None:
5105                if 1 <= len(args.trade) <= 5:
5106                    trader.Trade(
5107                        operation=args.trade[0],
5108                        lots=int(args.trade[1]) if len(args.trade) >= 2 else 1,
5109                        tp=float(args.trade[2]) if len(args.trade) >= 3 else 0.,
5110                        sl=float(args.trade[3]) if len(args.trade) >= 4 else 0.,
5111                        expDate=args.trade[4] if len(args.trade) == 5 else "Undefined",
5112                    )
5113
5114                else:
5115                    uLogger.error("You must specify 1-5 parameters to open trade: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
5116
5117            elif args.buy is not None:
5118                if 0 <= len(args.buy) <= 4:
5119                    trader.Buy(
5120                        lots=int(args.buy[0]) if len(args.buy) >= 1 else 1,
5121                        tp=float(args.buy[1]) if len(args.buy) >= 2 else 0.,
5122                        sl=float(args.buy[2]) if len(args.buy) >= 3 else 0.,
5123                        expDate=args.buy[3] if len(args.buy) == 4 else "Undefined",
5124                    )
5125
5126                else:
5127                    uLogger.error("You must specify 0-4 parameters to open buy position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
5128
5129            elif args.sell is not None:
5130                if 0 <= len(args.sell) <= 4:
5131                    trader.Sell(
5132                        lots=int(args.sell[0]) if len(args.sell) >= 1 else 1,
5133                        tp=float(args.sell[1]) if len(args.sell) >= 2 else 0.,
5134                        sl=float(args.sell[2]) if len(args.sell) >= 3 else 0.,
5135                        expDate=args.sell[3] if len(args.sell) == 4 else "Undefined",
5136                    )
5137
5138                else:
5139                    uLogger.error("You must specify 0-4 parameters to open sell position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
5140
5141            elif args.order:
5142                if 4 <= len(args.order) <= 7:
5143                    trader.Order(
5144                        operation=args.order[0],
5145                        orderType=args.order[1],
5146                        lots=int(args.order[2]),
5147                        targetPrice=float(args.order[3]),
5148                        limitPrice=float(args.order[4]) if len(args.order) >= 5 else 0.,
5149                        stopType=args.order[5] if len(args.order) >= 6 else "Limit",
5150                        expDate=args.order[6] if len(args.order) == 7 else "Undefined",
5151                    )
5152
5153                else:
5154                    uLogger.error("You must specify 4-7 parameters to open order: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]]. See: `python TKSBrokerAPI.py --help`")
5155
5156            elif args.buy_limit:
5157                trader.BuyLimit(lots=int(args.buy_limit[0]), targetPrice=args.buy_limit[1])
5158
5159            elif args.sell_limit:
5160                trader.SellLimit(lots=int(args.sell_limit[0]), targetPrice=args.sell_limit[1])
5161
5162            elif args.buy_stop:
5163                if 2 <= len(args.buy_stop) <= 7:
5164                    trader.BuyStop(
5165                        lots=int(args.buy_stop[0]),
5166                        targetPrice=float(args.buy_stop[1]),
5167                        limitPrice=float(args.buy_stop[2]) if len(args.buy_stop) >= 3 else 0.,
5168                        stopType=args.buy_stop[3] if len(args.buy_stop) >= 4 else "Limit",
5169                        expDate=args.buy_stop[4] if len(args.buy_stop) == 5 else "Undefined",
5170                    )
5171
5172                else:
5173                    uLogger.error("You must specify 2-5 parameters for buy stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
5174
5175            elif args.sell_stop:
5176                if 2 <= len(args.sell_stop) <= 7:
5177                    trader.SellStop(
5178                        lots=int(args.sell_stop[0]),
5179                        targetPrice=float(args.sell_stop[1]),
5180                        limitPrice=float(args.sell_stop[2]) if len(args.sell_stop) >= 3 else 0.,
5181                        stopType=args.sell_stop[3] if len(args.sell_stop) >= 4 else "Limit",
5182                        expDate=args.sell_stop[4] if len(args.sell_stop) == 5 else "Undefined",
5183                    )
5184
5185                else:
5186                    uLogger.error("You must specify 2-5 parameters for sell stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: python TKSBrokerAPI.py --help")
5187
5188            # elif args.buy_order_grid is not None:
5189            #     # update order grid work with api v2
5190            #     if len(args.buy_order_grid) == 2:
5191            #         orderParams = trader.ParseOrderParameters(operation="Buy", **dict(kw.split('=') for kw in args.buy_order_grid))
5192            #
5193            #         for order in orderParams:
5194            #             trader.Order(operation="Buy", lots=order["lot"], price=order["price"])
5195            #
5196            #     else:
5197            #         uLogger.error("To open grid of pending BUY limit-orders (below current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`")
5198            #
5199            # elif args.sell_order_grid is not None:
5200            #     # update order grid work with api v2
5201            #     if len(args.sell_order_grid) >= 2:
5202            #         orderParams = trader.ParseOrderParameters(operation="Sell", **dict(kw.split('=') for kw in args.sell_order_grid))
5203            #
5204            #         for order in orderParams:
5205            #             trader.Order(operation="Sell", lots=order["lot"], price=order["price"])
5206            #
5207            #     else:
5208            #         uLogger.error("To open grid of pending SELL limit-orders (above current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`")
5209
5210            elif args.close_order is not None:
5211                trader.CloseOrders(args.close_order)  # close only one order
5212
5213            elif args.close_orders is not None:
5214                trader.CloseOrders(args.close_orders)  # close list of orders
5215
5216            elif args.close_trade:
5217                if not (args.ticker or args.figi):
5218                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
5219                    raise Exception("Ticker or FIGI required")
5220
5221                if args.ticker:
5222                    trader.CloseTrades([str(args.ticker).upper()])  # close only one trade by ticker (priority)
5223
5224                else:
5225                    trader.CloseTrades([str(args.figi).upper()])  # close only one trade by FIGI
5226
5227            elif args.close_trades is not None:
5228                trader.CloseTrades(args.close_trades)  # close trades for list of tickers
5229
5230            elif args.close_all is not None:
5231                if args.ticker:
5232                    trader.CloseAllByTicker(instrument=str(args.ticker).upper())
5233
5234                elif args.figi:
5235                    trader.CloseAllByFIGI(instrument=str(args.figi).upper())
5236
5237                else:
5238                    trader.CloseAll(*args.close_all)
5239
5240            elif args.limits:
5241                if args.output is not None:
5242                    trader.withdrawalLimitsFile = args.output
5243
5244                trader.OverviewLimits(show=True)
5245
5246            elif args.user_info:
5247                if args.output is not None:
5248                    trader.userInfoFile = args.output
5249
5250                trader.OverviewUserInfo(show=True)
5251
5252            elif args.account:
5253                if args.output is not None:
5254                    trader.userAccountsFile = args.output
5255
5256                trader.OverviewAccounts(show=True)
5257
5258            else:
5259                uLogger.error("There is no command to execute! One of the possible commands must be selected. See help with `--help` key.")
5260                raise Exception("There is no command to execute")
5261
5262    except Exception:
5263        trace = tb.format_exc()
5264        for e in ["socket.gaierror", "nodename nor servname provided", "or not known", "NewConnectionError", "[Errno 8]", "Failed to establish a new connection"]:
5265            if e in trace:
5266                uLogger.error("Check your Internet connection! Failed to establish connection to broker server!")
5267                break
5268
5269        uLogger.debug(trace)
5270        uLogger.debug("Please, check issues or request a new one at https://github.com/Tim55667757/TKSBrokerAPI/issues")
5271        exitCode = 255  # an error occurred, must be open a ticket for this issue
5272
5273    finally:
5274        finish = datetime.now(tzutc())
5275
5276        if exitCode == 0:
5277            if args.more:
5278                uLogger.debug("All operations were finished success (summary code is 0).")
5279
5280        else:
5281            uLogger.error("An issue occurred with TKSBrokerAPI module! See full debug log in [{}] or run TKSBrokerAPI once again with the key `--debug-level 10`. Summary code: {}".format(
5282                os.path.abspath(uLog.defaultLogFile), exitCode,
5283            ))
5284
5285        uLogger.debug(">>> TKSBrokerAPI module work duration: [{}]".format(finish - start))
5286        uLogger.debug(">>> TKSBrokerAPI module finished: [{} UTC], it is [{}] local time".format(
5287            finish.strftime(TKS_PRINT_DATE_TIME_FORMAT),
5288            finish.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
5289        ))
5290        uLogger.debug("=-" * 50)
5291
5292        if not kwargs:
5293            sys.exit(exitCode)
5294
5295        else:
5296            return exitCode

Main function for work with TKSBrokerAPI in the console.

See examples: